<?php namespace App\Classes; use Exception; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use App\Classes\File\{Receive,Send}; use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketException; use App\Models\{Address,Setup}; abstract class Protocol { // Enable extra debugging protected bool $DEBUG = FALSE; private const LOGKEY = 'P--'; // Return constants protected const OK = 0; protected const EOF = -1; protected const TIMEOUT = -2; protected const RCDO = -3; protected const GCOUNT = -4; protected const ERROR = -5; // Our sessions Types public const SESSION_AUTO = 0; public const SESSION_EMSI = 1; public const SESSION_BINKP = 2; public const SESSION_ZMODEM = 3; protected const MAX_PATH = 1024; /* 9 most right bits are zeros */ private const O_BASE = 9; /* First 9 bits are protocol */ protected const O_NRQ = (1<<self::O_BASE); /* 0000 0000 0000 0000 0010 0000 0000 BOTH - No file requests accepted by this system */ protected const O_HRQ = (1<<(self::O_BASE+1)); /* 0000 0000 0000 0000 0100 0000 0000 BOTH - Hold file requests (not processed at this time). */ protected const O_FNC = (1<<(self::O_BASE+2)); /* 0000 0000 0000 0000 1000 0000 0000 - Filename conversion, transmitted files must be 8.3 */ protected const O_XMA = (1<<(self::O_BASE+3)); /* 0000 0000 0000 0001 0000 0000 0000 - Supports other forms of compressed mail */ protected const O_HAT = (1<<(self::O_BASE+4)); /* 0000 0000 0000 0010 0000 0000 0000 BOTH - Hold ALL files (Answering System) */ protected const O_HXT = (1<<(self::O_BASE+5)); /* 0000 0000 0000 0100 0000 0000 0000 BOTH - Hold Mail traffic */ protected const O_NPU = (1<<(self::O_BASE+6)); /* 0000 0000 0000 1000 0000 0000 0000 - No files pickup desired (Calling System) */ protected const O_PUP = (1<<(self::O_BASE+7)); /* 0000 0000 0001 0000 0000 0000 0000 - Pickup files for primary address only */ protected const O_PUA = (1<<(self::O_BASE+8)); /* 0000 0000 0010 0000 0000 0000 0000 EMSI - Pickup files for all presented addresses */ protected const O_PWD = (1<<(self::O_BASE+9)); /* 0000 0000 0100 0000 0000 0000 0000 BINK - Node needs to be password validated */ protected const O_BAD = (1<<(self::O_BASE+10)); /* 0000 0000 1000 0000 0000 0000 0000 BOTH - Node invalid password presented */ protected const O_RH1 = (1<<(self::O_BASE+11)); /* 0000 0001 0000 0000 0000 0000 0000 EMSI - Use RH1 for Hydra (files-after-freqs) */ protected const O_LST = (1<<(self::O_BASE+12)); /* 0000 0010 0000 0000 0000 0000 0000 BOTH - Node is nodelisted */ protected const O_INB = (1<<(self::O_BASE+13)); /* 0000 0100 0000 0000 0000 0000 0000 BOTH - Inbound session */ protected const O_TCP = (1<<(self::O_BASE+14)); /* 0000 1000 0000 0000 0000 0000 0000 BOTH - TCP session */ protected const O_EII = (1<<(self::O_BASE+15)); /* 0001 0000 0000 0000 0000 0000 0000 EMSI - Remote understands EMSI-II */ // Session Status protected const S_OK = 0; protected const S_NODIAL = 1; protected const S_REDIAL = 2; protected const S_BUSY = 3; protected const S_FAILURE = 4; protected const S_MASK = 7; protected const S_HOLDR = 8; protected const S_HOLDX = 16; protected const S_HOLDA = 32; protected const S_ADDTRY = 64; protected const S_ANYHOLD = (self::S_HOLDR|self::S_HOLDX|self::S_HOLDA); // File transfer status protected const FOP_OK = 0; protected const FOP_CONT = 1; protected const FOP_SKIP = 2; protected const FOP_ERROR = 3; protected const FOP_SUSPEND = 4; protected const MO_CHAT = 4; protected SocketClient $client; /* Our socket details */ protected ?Setup $setup; /* Our setup */ protected Node $node; /* The node we are communicating with */ protected Send $send; /* The list of files we are sending */ protected Receive $recv; /* The list of files we are receiving */ private int $options; /* Our options for a session */ private int $session; /* Tracks where we are up to with this session */ protected bool $originate; /* Are we originating a connection */ private array $comms; /* Our comms details */ abstract protected function protocol_init(): int; abstract protected function protocol_session(): int; public function __construct(Setup $o=NULL) { if ($o && ! $o->system->addresses->count()) throw new Exception('We dont have any FTN addresses assigned'); $this->setup = $o; } /** * @throws Exception */ public function __get($key) { switch ($key) { case 'ls_SkipGuard': /* double-skip protection on/off */ case 'rxOptions': /* Options from ZRINIT header */ case 'socket_error': return $this->comms[$key] ?? 0; case 'ls_rxAttnStr': return $this->comms[$key] ?? ''; default: throw new Exception('Unknown key: '.$key); } } /** * @throws Exception */ public function __set($key,$value) { switch ($key) { case 'ls_rxAttnStr': case 'ls_SkipGuard': case 'rxOptions': case 'socket_error': $this->comms[$key] = $value; break; default: throw new Exception('Unknown key: '.$key); } } /** * We got an error, close anything we are have open * * @throws Exception */ protected function error_close(): void { if ($this->send->fd) $this->send->close(FALSE); if ($this->recv->fd) $this->recv->close(); } /** * Incoming Protocol session * * @param SocketClient $client * @return int|null * @throws SocketException */ public function onConnect(SocketClient $client): ?int { $pid = pcntl_fork(); if ($pid == -1) throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process'); Log::debug(sprintf('%s:= End [%d]',self::LOGKEY,$pid)); // Parent return ready for next connection return $pid; } protected function optionClear(int $key): void { $this->options &= ~$key; } protected function optionGet(int $key): int { return ($this->options & $key); } protected function optionSet(int $key): void { $this->options |= $key; } /** * Our addresses to send to the remote * @return Collection */ protected function our_addresses(): Collection { if ($this->setup->optionGet(Setup::O_HIDEAKA)) { $addresses = collect(); foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao) $addresses = $addresses->merge($this->setup->system->match($ao->zone,Address::NODE_ZC|Address::NODE_RC|Address::NODE_NC|Address::NODE_HC|Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_POINT)); $addresses = $addresses->unique(); Log::debug(sprintf('%s: - Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); } else { $addresses = $this->setup->system->addresses; Log::debug(sprintf('%s: - Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); } return $addresses; } /** * Initialise our Session * * @param int $type * @param SocketClient $client * @param Address|null $o * @return int * @throws Exception */ public function session(int $type,SocketClient $client,Address $o=NULL): int { if ($o->exists) Log::withContext(['ftn'=>$o->ftn]); Log::debug(sprintf('%s:+ Start [%d]',self::LOGKEY,$type)); // This sessions options $this->options = 0; $this->session = 0; // Our files that we are sending/receive $this->send = new Send; $this->recv = new Receive; if ($o) { // The node we are communicating with $this->node = new Node; $this->originate = $o->exists; // If we are connecting to a node if ($o->exists) { $this->node->originate($o); } else { $this->optionSet(self::O_INB); } } // We are an IP node $this->optionSet(self::O_TCP); $this->setClient($client); switch ($type) { /** @noinspection PhpMissingBreakStatementInspection */ case self::SESSION_AUTO: Log::debug(sprintf('%s: - Trying EMSI',self::LOGKEY)); $rc = $this->protocol_init(); if ($rc < 0) { Log::error(sprintf('%s: ! Unable to start EMSI [%d]',self::LOGKEY,$rc)); return self::S_REDIAL | self::S_ADDTRY; } case self::SESSION_EMSI: Log::debug(sprintf('%s: - Starting EMSI',self::LOGKEY)); $rc = $this->protocol_session(); break; case self::SESSION_BINKP: Log::debug(sprintf('%s: - Starting BINKP',self::LOGKEY)); $rc = $this->protocol_session(); break; case self::SESSION_ZMODEM: Log::debug(sprintf('%s: - Starting ZMODEM',self::LOGKEY)); $this->client->speed = SocketClient::TCP_SPEED; $this->originate = FALSE; return $this->protocol_session(); default: Log::error(sprintf('%s: ! Unsupported session type [%d]',self::LOGKEY,$type)); return self::S_REDIAL | self::S_ADDTRY; } // @todo Unlock outbounds // @todo These flags determine when we connect to the remote. // If the remote indicated that they dont support file requests (NRQ) or temporarily hold them (HRQ) if (($this->node->optionGet(self::O_NRQ) && (! $this->setup->ignore_nrq)) || $this->node->optionGet(self::O_HRQ)) $rc |= self::S_HOLDR; if ($this->optionGet(self::O_HXT)) $rc |= self::S_HOLDX; if ($this->optionGet(self::O_HAT)) $rc |= self::S_HOLDA; Log::info(sprintf('%s: Total: %s - %d:%02d:%02d online, (%d) %lu%s sent, (%d) %lu%s received - %s', self::LOGKEY, $this->node->address ? $this->node->address->ftn : 'Unknown', $this->node->session_time/3600, $this->node->session_time%3600/60, $this->node->session_time%60, $this->send->total_sent,$this->send->total_sent_bytes,'b', $this->recv->total_recv,$this->recv->total_recv_bytes,'b', (($rc & self::S_MASK) == self::S_OK) ? 'Successful' : 'Failed', )); // @todo Log to history log in the DB. //if ($this->node->start_time && $this->setup->cfg('CFG_HISTORY')) {} // @todo Optional after session execution event // if ($this->node->start_time && $this->setup->cfg('CFG_AFTERSESSION')) {} // @todo Optional after session includes mail event // if ($this->node->start_time && $this->setup->cfg('CFG_AFTERMAIL')) {} return ($rc & ~self::S_ADDTRY); } /** * Clear a session bit * * @param int $key */ protected function sessionClear(int $key): void { $this->session &= ~$key; } /** * Get a session bit * @param int $key * @return bool */ protected function sessionGet(int $key): bool { return ($this->session & $key); } /** * Set a session bit (with SE_*) * * @param int $key */ protected function sessionSet(int $key): void { $this->session |= $key; } /** * Set our client that we are communicating with * * @param SocketClient $client */ private function setClient(SocketClient $client): void { $this->client = $client; } }