#!/usr/local/bin/php53
<?php

/*

vmodemd, the virtual modem daemon
=================================

Copyright (C) 2009 Vincent Negrier aka. sIX <six at aegis-corp.org>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

*/

namespace VModem;

const VERSION = "1.2.2";

const DEFAULT_CONFIG_FILE = "/etc/vmodemd/vmodemd.conf";
const DEFAULT_LISTEN_ADDR = "0.0.0.0";
const DEFAULT_LISTEN_PORT = 1337;

require "inc/nanoserv2/nanoserv.php";

use \Exception;
use \Nanoserv;
use \NS_Connection_Handler as Nanoserv_Handler;

class Modem_Busy_Exception extends Exception {

	public function __construct() {

		parent::__construct("all modems are busy");
	
	}

}

class Modem_Device_Error_Exception extends Exception {

	public function __construct($devname) {

		parent::__construct("cannot open device '{$devname}'");
	
	}

}

class Posix_Exception extends Exception {

	public function __construct($str) {

		parent::__construct("posix: {$str}");
	
	}

}

class Posix_Group_Exception extends Posix_Exception {

	public function __construct($group) {

		parent::__construct("cannot set group to '{$group}'");
	
	}

}

class Posix_User_Exception extends Posix_Exception {

	public function __construct($user) {

		parent::__construct("cannot set user to '{$user}'");
	
	}

}

class Connection extends Nanoserv_Handler {

	const DEFAULT_ANIM_CHARS = '/-\|';

	static public $translate = array(	"\r\n"	=> "\r",
										"\r\0"	=> "\r",
										"\n"	=> "\r");
	
	public $modem;
	public $conf;

	public $peer;
	public $connect_t;
	public $xfer_in = 0;
	public $xfer_out = 0;

	public $wait_anim_timer;
	public $anim_i = 0;
	
	public function __construct($conf) {

		$this->conf = $conf;
	
	}
	
	public function on_Accept() {

		$this->peer = $this->socket->Get_Peer_Name();
		$this->connect_t = time();
		
		if (!$this->conf["global"]["quiet"]) {
		
			$banner = base64_decode("G1s/N2gbWzQwbRtbMkobWzFDG1swOzFtX19fIF9fXxtbMTlDX18bWzE5Q19fDQobWzM2bXwgICBZICAgLi0tLS0tLS0tLi0tLS0tLi0tfCAgLi0tLS0tLi0tLS0tLS0tLi0tfCAgfA0KG1swOzM2bXwgICB8ICAgfBtbOEN8ICBfICB8ICBfICB8ICAtX198G1s4Q3wgIF8gIHwNChtbMTszMG18ICAgfCAgIHxfX3xfX3xfX3xfX19fX3xfX19fX3xfX19fX3xfX3xfX3xfX3xfX19fX3wNCnwgICAxICAgfA0KIFw6Li4gLi8gIBtbMDszNm12bRtbMW1vG1swOzM2bWRlbWQbWzFtLxtbMDszNm1AVkVSU0lPTkAgKBtbMW1jG1swOzM2bSkgMm8bWzFtbxtbMDszNm05IGJ5IHMbWzFtSRtbMDszNm1YIC8gYUVHG1sxbWkbWzA7MzZtUw0KICAbWzE7MzBtYC0tLScgICBodHRwOi8vZGVhZGxpbmUuYWVnaXMtY29ycC5vcmcvdm1vZGVtZA0KG1swbRtbMjU1RA0K");

			$init = str_replace("@VERSION@", VERSION, $banner);
			
		} else {

			$init = "";

		}
		
		if ($this->conf["global"]["telnet_init"]) {
		
			// Telnet - enable 8 bit mode
			$init .= chr(255) . chr(251) . chr(0);
			
			// Telnet - suppress go ahead
			$init .= chr(255) . chr(251) . chr(3);
			
			// Telnet - disable local echo
			$init .= chr(255) . chr(251) . chr(1);
			$init .= chr(255) . chr(254) . chr(1);

			// Telnet - disable line mode
			$init .= chr(255) . chr(254) . chr(34);
			
			// ANSI - Back to column 1
			$init .= chr(27) . "[80D";
			
			// ANSI - Clear end of line
			$init .= chr(27) . "[2K";
		
		}
		
		if ($init) {
		
			$this->Write($init);
		
			Nanoserv::New_Timer(1, array($this, "Start"));

		} else {

			$this->Start();

		}
	
	}
	
	public function Stop_Wait_Anim() {

		$this->wait_anim_timer->Deactivate();
	
	}
	
	public function Do_Wait_Anim() {

		if (!$this->modem->carrier) {

			$tmp = $this->conf["global"]["anim_chars"] ?: self::DEFAULT_ANIM_CHARS;

			$this->Write(chr(8) . $tmp[$this->anim_i]);

			if (++$this->anim_i >= strlen($tmp)) {

				$this->anim_i = 0;

			}
			
			$this->wait_anim_timer = Nanoserv::New_Timer(1, array($this, "Do_Wait_Anim"));
		
		}
	
	}
	
	public function Start() {

		try {

			if (!$this->socket->connected) return false;
		
			$this->modem = Modem::Get_Available();

			if ($connect_scr = $this->modem->connect_scr) {

				$peer_ip = strtok($this->peer, ":");
				$peer_port = strtok("");
				
				exec("{$connect_scr} {$peer_ip} {$peer_port} {$this->modem->id}");

			}
			
			$this->modem->Connect();
			
			if (isset($this->modem->title)) {

				$connect_txt  = chr(27) . "]2;" . $this->modem->title . chr(7) .chr(27) . "[80D" . chr(27) . "[2K";
			
			} else {

				$connect_txt = "";

			}
			
			if ($connect_file = $this->modem->connect_txt) {

				$connect_txt .= @file_get_contents($connect_file);

			}
			
			techo("connecting {$this->peer} with vmodem #{$this->modem->id}");
			
			if ($connect_txt) {
				
				$this->Write($connect_txt);

			}
		
			if ($this->conf["global"]["load_anim"]) {
			
				Nanoserv::New_Timer(1, array($this, "Do_Wait_Anim"));

			}
		
		} catch (Modem_Busy_Exception $e) {

			if ($busy_file = $GLOBALS["conf"]["global"]["busy_txt"]) {

				$busy_txt = @file_get_contents($busy_file);

			} else {

				$busy_txt = $e->getMessage() . "\n";

			}
			
			$this->Write($busy_txt, array($this, "Disconnect"));

			techo("cannot connect {$this->peer} with vmodem: {$e->getMessage()}");
		
		} catch (Modem_Device_Error_Exception $e) {

			if ($deverr_file = $GLOBALS["conf"]["global"]["deverr_txt"]) {

				$deverr_txt = @file_get_contents($deverr_file);

			} else {

				$deverr_txt = $e->getMessage() . "\n";

			}
			
			$this->Write($deverr_txt, array($this, "Disconnect"));

			$this->modem->Reset();
		
			techo("cannot connect {$this->peer} with vmodem: {$e->getMessage()}");
		
		} catch (Posix_Exception $e) {

			techo("cannot connect {$this->peer} with vmodem: {$e->getMessage()}");
			exit(); // because we are actually in the child process
		
		}

	}
	
	public function on_Read($data) {

		if (isset($this->modem)) {
		
			$data = strtr($data, self::$translate);
			
			$this->xfer_in += strlen($data);			
	
			$this->modem->Write($data);

		}
	
	}
	
	public function on_Disconnect() {

		if (isset($this->modem)) {
		
			$this->modem->Hangup();

		}
	
		list($tot_time, $tot_in, $tot_out) = $this->Get_Stats();
		techo("connection reset by peer {$this->peer}: online {$tot_time} sec, {$tot_in}/{$tot_out} bytes in/out");
	
		if ($hangup_scr = $this->modem->hangup_scr) {

			exec("{$hangup_scr} {$this->modem->id} 1");

		}

	}

	public function Write($data, $callback = false) {

		$this->xfer_out += strlen($data);

		parent::Write($data, $callback);
	
	}

	public function Disconnect() {

		if (isset($this->modem)) {
		
			$this->modem->Hangup();

		}

		parent::Disconnect();
	
		list($tot_time, $tot_in, $tot_out) = $this->Get_Stats();
		techo("connection closed {$this->peer}: online {$tot_time} sec, {$tot_in}/{$tot_out} bytes in/out");
	
		if ($hangup_scr = $this->modem->hangup_scr) {

			exec("{$hangup_scr} {$this->modem->id} 0");

		}
	
	}

	public function Get_Stats() {

		$tot_time = number_format(time() - $this->connect_t);
		$tot_in = number_format($this->xfer_in);
		$tot_out = number_format($this->xfer_out);

		return array($tot_time, $tot_in, $tot_out);
	
	}

}

class Modem {

	const STATUS_WAIT = 1;
	const STATUS_INUSE = 2;
	const STATUS_ERROR = 3;
	
	static public $all = array();
	
	public $id;
	public $device;
	public $tty;
	public $status = self::STATUS_WAIT;
	public $serial_app;
	public $carrier = false;

	public $user;
	public $group;
	public $command;
	public $args = array();
	public $env = array();
	public $connect_scr;
	public $hangup_scr;
	public $connect_txt;
	public $hangup_txt;
	public $title;
	
	static public function Initialize(array $conf) {

		$vmodems = $conf["global"]["vmodems"];

		if (!$vmodems) {

			throw new Exception("invalid 'vmodems' global configuration directive");

		}

		for ($i = 1; $i <= $vmodems; $i++) {

			if (!is_array($mconf = $conf["modem{$i}"])) {

				throw new Exception("configuration section 'modem{$i}' not found");

			}
			
			$device = $mconf["device"];

			if (!$device) {

				throw new Exception("invalid 'device' configuration directive in section 'modem{$i}'");

			}

			$command = $mconf["command"];

			if (!$command) {

				throw new Exception("invalid 'command' configuration directive in section 'modem{$i}'");

			}
			
			$args = array();
			$j = 1;
			
			while (isset($mconf["arg{$j}"])) {

				$args[] = $mconf["arg{$j}"];
				$j++;
			
			}
			
			$env = array();

			foreach ($mconf as $k => $v) {
				
				if (substr($k, 0, 3) === "env") {

					$env_name = substr($k, 3);
					$env_val = $v;

					$env[$env_name] = $env_val;
				
				}
			
			}
			
			$modem = new self($i, $device, $command);

			if ($mconf["user"]) $modem->user = $mconf["user"];
			if ($mconf["group"]) $modem->group = $mconf["group"];
			if ($mconf["connect_scr"]) $modem->connect_scr = $mconf["connect_scr"];
			if ($mconf["hangup_scr"]) $modem->hangup_scr = $mconf["hangup_scr"];
			if ($mconf["connect_txt"]) $modem->connect_txt = $mconf["connect_txt"];
			if ($mconf["hangup_txt"]) $modem->hangup_txt = $mconf["hangup_txt"];
			if ($mconf["title"]) $modem->title = $mconf["title"];

			$modem->args = $args;
			$modem->env = $env;

		}

	}
	
	static public function Get_Available() {

		foreach (self::$all as $modem) {

			if ($modem->status === self::STATUS_WAIT) {

				$modem->status = self::STATUS_INUSE;
				
				return $modem;

			}
		
		}

		throw new Modem_Busy_Exception();

	}

	static public function Get_All_Inuse() {

		$ret = array();
		
		foreach (self::$all as $modem) {

			if ($modem->status === self::STATUS_INUSE) {

				$ret[] = $modem;

			}
		
		}

		return $ret;
	
	}
	
	private function __construct($id, $device, $command) {

		$this->id = $id;
		$this->device = $device;
		$this->command = $command;

		self::$all[$id] = $this;

	}

	public function Connect() {

		$this->tty = @fopen($this->device, "w+");

		if (!$this->tty) {

			$this->status = self::STATUS_ERROR;
			
			throw new Modem_Device_Error_Exception($this->device);

		}

		stream_set_blocking($this->tty, false);
		
		$this->serial_app = new Serial_App($this->command, $this->user, $this->group, $this->args, $this->env);
		$this->serial_app->Launch();
	
	}

	public function Read() {

		$ret = fread($this->tty, 8192);

		if ($ret !== "") $this->carrier = true;

		return $ret;
	
	}
	
	public function Write($data) {

		if (($this->status === self::STATUS_INUSE) && ($this->serial_app->status === Serial_App::STATUS_RUNNING)) {

			$ret = fwrite($this->tty, $data);

			if ($ret !== strlen($data)) {

				techo("WARN: lost data while sending to modem");

			}
			
			return $ret;
		
		} else {

			techo("WARN: sending data when not ready ({$this->status},{$this->serial_app->status})");
			
			return false;

		}
	
	}
	
	public function Reset() {

		$this->status = self::STATUS_WAIT;
		$this->carrier = false;

		if ($this->serial_app) {

			$this->serial_app->Kill();

		}
		
		if (isset($this->tty)) {

			@fclose($this->tty);
			unset($this->tty);

		}
	
	}

	public function Hangup() {

		if ($this->status === self::STATUS_INUSE) {
		
			$this->Reset();

		}

	}

	public function Get_Connection() {

		foreach (Nanoserv::Get_Connections() as $c) {

			if (($c instanceof Connection) && ($c->modem === $this)) {

				return $c;
			
			}
		
		}

		return false;
	
	}

}

class Serial_App {

	const STATUS_WAIT = 1;
	const STATUS_RUNNING = 2;
	const STATUS_FINISHED = 3;
	const STATUS_ERROR = 7;
	
	public $command;
	public $args = array();
	public $env = array();
	public $user;
	public $group;
	public $status = self::STATUS_WAIT;
	public $pid;

	public function __construct($command, $user, $group, $args = array(), $env = array()) {

		$this->command = $command;
		$this->user = $user;
		$this->group = $group;
		$this->args = $args;
		$this->env = $env;
	
	}

	public function Launch() {

		$this->pid = Nanoserv::Fork();

		if ($this->pid === 0) {

			if ($this->group) {

				if ((!$tmp = posix_getgrnam($this->group)) || (!posix_setgid($tmp["gid"]))) {

					throw new Posix_Group_Exception($this->group);

				}

				if ((!$tmp = posix_getpwnam($this->user)) || (!posix_setuid($tmp["uid"]))) {

					throw new Posix_User_Exception($this->user);

				}
			
			}
			
			pcntl_exec($this->command, $this->args, $this->env);

		} else if ($this->pid > 0) {

			$this->status = self::STATUS_RUNNING;

		} else {

			$this->status = self::STATUS_ERROR;

		}
	
	}

	public function Kill() {

		posix_kill($this->pid, SIGTERM);
	
	}

}

function techo($s) {

	echo date("Ymd:His") . " {$s}\n";

}

try {

	techo("vmodemd/" . VERSION . " (c) 2009 by sIX / aEGiS <six at aegis-corp.org>");

	$opts = getopt("c:");

	$conffile = $opts["c"] ?: DEFAULT_CONFIG_FILE;

	if (!$conf = @parse_ini_file($conffile, true)) {

		techo("cannot read configuration file '{$conffile}', use -c <conffile>");
		exit(-1);

	}

	Modem::Initialize($conf);

	$laddr = $conf["global"]["listen_addr"] ?: DEFAULT_LISTEN_ADDR;
	$lport = $conf["global"]["listen_port"] ?: DEFAULT_LISTEN_PORT;

	$lstr = "tcp://{$laddr}:{$lport}";

	Nanoserv::New_Listener($lstr, __NAMESPACE__ . "\\Connection", $conf)->Activate();

} catch (Exception $e) {

	techo("FATAL: {$e->getMessage()}");
	exit(-2);

}

$run = true;

while ($run) {

	$modems = Modem::Get_All_Inuse();
	$mttys = $mttys_lookup = array();
	$mttys[0] = array();
	
	foreach ($modems as $modem) {

		$mttys[0][] = $modem->tty;
		$mttys_lookup[(int)$modem->tty] = $modem;
	
	}
	
	$ret = Nanoserv::Run(1, $mttys);

	if ($ret[0]) {
	
		foreach ($ret[0] as $mtty) {

			$modem = $mttys_lookup[(int)$mtty];

			$data = $modem->Read();

			if ($c = $modem->Get_Connection()) {

				if ($data === "") {

					if ($hangup_file = $modem->hangup_txt) {

						$hangup_txt = @file_get_contents($hangup_file);

					} else {

						$hangup_txt = "NO CARRIER\r\n";

					}
					
					$modem->Reset();
					$c->Write($hangup_txt, array($c, "Disconnect"));

				} else {
				
					$data = str_replace(chr(255), chr(255) . chr(255), $data);
					
					$c->Write($data);

				}

			}
		
		}
	
	}

}

/*

                    s                                   s
                   .$                                  .$
                  .$$                                 .$$
                 .$^$                                 $$$
                .$' $.    .s$Ss..       .s$Ss..       `$$     .s$Ss..
               .$'  $$  .$$$ýø^ý$$s.  .$$$ýø^ý$$s.     `$.  .$$$ýø^ý$$s.
             .$$'   $$  $$$      `$$  $$$      `$$      $$  $$$      `$$
          .s$$$$$s. ý$. `ý$$s.     ý  $$$        ý      $$. `ý$$s.     ý
       .sS$ýø^øýS$$Ss.ý    `$$$ý'     $$$.        .sS.  $$$    `øýS$Ss.
 ..ssS$$ý'        `øýS$s.  .$$'       `$$$     .s$SýS$. $$$.    .s. `øý$s.
 ý$Sý'              $Sss. .$$'      .$ $$$. .s$Sý'.$ $$ $$$$. .$$'     `$$
                    $$$$$ $$$      .$$ `$$$ `ýý' .$$ `$ $$$$$ $$$      .$$
                    $$$$ý `$$s.  .s$$'  `$$s.  .s$$'    $$$$ý `$$s.  .s$$'
                    $$ý     `ý$$$Sý'  Ms. `ý$$$Sý'      $$ý     `ý$$$Sý'
                    $'                                  $'
                          C       O       R       P.

               - finally dominating the PCBoard scene in 2oo9 -

*/

?>
