<?php namespace App\Classes; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use App\Classes\File\{Receive,Send}; use App\Classes\Protocol\EMSI; use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketException; use App\Models\{Address,Mailer,Setup,System,SystemLog}; // @todo after receiving a mail packet/file, dont acknowledge it until we can validate that we can read it properly. abstract class Protocol { // Enable extra debugging protected const DEBUG = FALSE; private const LOGKEY = 'P--'; /* CONSTS */ // Return constants protected const OK = 0; protected const TIMEOUT = -2; protected const RCDO = -3; protected const ERROR = -5; protected const MAX_PATH = 1024; /* O_ options - [First 9 bits are protocol P_* (Interfaces/ZModem)] */ /** 0000 0000 0000 0000 0010 0000 0000 BOTH - No file requests accepted by this system */ protected const O_NRQ = 1<<9; /** 0000 0000 0000 0000 0100 0000 0000 BOTH - Hold file requests (not processed at this time). */ protected const O_HRQ = 1<<10; /** 0000 0000 0000 0000 1000 0000 0000 - Filename conversion, transmitted files must be 8.3 */ protected const O_FNC = 1<<11; /** 0000 0000 0000 0001 0000 0000 0000 - Supports other forms of compressed mail */ protected const O_XMA = 1<<12; /** 0000 0000 0000 0010 0000 0000 0000 BOTH - Hold ALL files (Answering System) */ protected const O_HAT = 1<<13; /** 0000 0000 0000 0100 0000 0000 0000 BOTH - Hold Mail traffic */ protected const O_HXT = 1<<14; /** 0000 0000 0000 1000 0000 0000 0000 - No files pickup desired (Calling System) */ protected const O_NPU = 1<<15; /** 0000 0000 0001 0000 0000 0000 0000 - Pickup files for primary address only */ protected const O_PUP = 1<<16; /** 0000 0000 0010 0000 0000 0000 0000 EMSI - Pickup files for all presented addresses */ protected const O_PUA = 1<<17; /** 0000 0000 0100 0000 0000 0000 0000 BINK - Node needs to be password validated */ protected const O_PWD = 1<<18; /** 0000 0000 1000 0000 0000 0000 0000 BOTH - Node invalid password presented */ protected const O_BAD = 1<<19; /** 0000 0001 0000 0000 0000 0000 0000 EMSI - Use RH1 for Hydra (files-after-freqs) */ protected const O_RH1 = 1<<20; /** 0000 0010 0000 0000 0000 0000 0000 BOTH - Node is nodelisted */ protected const O_LST = 1<<21; /** 0000 0100 0000 0000 0000 0000 0000 BOTH - Inbound session */ protected const O_INB = 1<<22; /** 0000 1000 0000 0000 0000 0000 0000 BOTH - TCP session */ protected const O_TCP = 1<<23; /** 0001 0000 0000 0000 0000 0000 0000 EMSI - Remote understands EMSI-II */ protected const O_EII = 1<<24; /* Negotiation Options */ /** 00 0000 I/They dont want a capability? */ protected const O_NO = 0; /** 00 0001 - I want a capability, but can be persuaded */ protected const O_WANT = 1<<0; /** 00 0010 - They want a capability and we want it too */ protected const O_WE = 1<<1; /** 00 0100 - They want a capability */ protected const O_THEY = 1<<2; /** 00 1000 - I want a capability, and wont compromise */ protected const O_NEED = 1<<3; /** 01 0000 - Extra options set */ protected const O_EXT = 1<<4; /** 10 0000 - We agree on a capability and we are set to use it */ protected const O_YES = 1<<5; // Session Status public const S_OK = 0; protected const S_NODIAL = 1; protected const S_REDIAL = 2; protected const S_BUSY = 3; protected const S_FAILURE = 4; public 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 public const FOP_OK = 0; public const FOP_CONT = 1; public const FOP_SKIP = 2; public const FOP_ERROR = 3; public const FOP_SUSPEND = 4; public const FOP_GOT = 5; public const TCP_SPEED = 115200; protected SocketClient $client; /* Our socket details */ protected ?Setup $setup; /* Our setup */ protected Node $node; /* The node we are communicating with */ /** The list of files we are sending */ protected Send $send; /** The list of files we are receiving */ protected Receive $recv; /** @var int The active options for a session */ private int $options; /** @var int Tracks the session state */ private int $session; /** @var array Our negotiated capability for a protocol session */ protected array $capability; // @todo make private /** @var bool Are we originating a connection */ protected bool $originate; /** Our comms details */ private array $comms; protected bool $force_queue = FALSE; abstract protected function protocol_init(): int; abstract protected function protocol_session(bool $force_queue=FALSE): int; public function __construct(Setup $o=NULL) { if ($o && ! $o->system->akas->count()) throw new \Exception('We dont have any ACTIVE 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 */ 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': $this->comms[$key] = $value; break; case 'client': $this->{$key} = $value; break; default: throw new \Exception('Unknown key: '.$key); } } /* Capabilities are what we negotitate with the remote and are valid for the session */ /** * Clear a capability bit * * @param int $cap (F_*) * @param int $val (O_*) * @return void */ public function capClear(int $cap,int $val): void { if (! array_key_exists($cap,$this->capability)) $this->capability[$cap] = 0; $this->capability[$cap] &= ~$val; } /** * Get a session bit (SE_*) * * @param int $cap (F_*) * @param int $val (O_*) * @return bool */ protected function capGet(int $cap,int $val): bool { if (! array_key_exists($cap,$this->capability)) $this->capability[$cap] = 0; if ($val === self::O_WE) return $this->capGet($cap,self::O_WANT) && $this->capGet($cap,self::O_THEY); return $this->capability[$cap] & $val; } /** * Set a session bit (SE_*) * * @param int $cap (F_*) * @param int $val (O_*) */ protected function capSet(int $cap,int $val): void { if (! array_key_exists($cap,$this->capability) || $val === 0) $this->capability[$cap] = 0; $this->capability[$cap] |= $val; } /** * 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,$this->node); 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'); if ($pid) Log::info(sprintf('%s:+ New connection, thread [%d] created',self::LOGKEY,$pid)); // Parent return ready for next connection return $pid; } /* O_* determine what features processing is availabile */ /** * Clear an option bit (O_*) * * @param int $key * @return void */ protected function optionClear(int $key): void { $this->options &= ~$key; } /** * Get an option bit (O_*) * * @param int $key * @return int */ protected function optionGet(int $key): int { return ($this->options & $key); } /** * Set an option bit (O_*) * * @param int $key * @return void */ 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,'options_options')) { $addresses = collect(); foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao) $addresses = $addresses->merge(our_address($ao->zone->domain)); $addresses = $addresses->unique(); Log::debug(sprintf('%s:- Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); } else { $addresses = $this->setup->system->akas; Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); } return $addresses; } /** * Initialise our Session * * @param Mailer $mo * @param SocketClient $client * @param Address|null $o * @return int * @throws \Exception */ public function session(Mailer $mo,SocketClient $client,Address $o=NULL): int { if ($o->exists) Log::withContext(['ftn'=>$o->ftn]); // This sessions options $this->options = 0; $this->session = 0; $this->capability = []; // 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) { Log::debug(sprintf('%s:+ Originating a connection to [%s]',self::LOGKEY,$o->ftn)); $this->node->originate($o); } else { $this->optionSet(self::O_INB); } } // We are an IP node $this->optionSet(self::O_TCP); $this->client = $client; switch ($mo->name) { case 'EMSI': Log::debug(sprintf('%s:- Starting 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_FAILURE; } $rc = $this->protocol_session($this->originate); break; case 'BINKP': Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY)); $rc = $this->protocol_session($this->originate); break; default: Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$mo->id)); return self::S_FAILURE; } // @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->optionGet(EMSI::F_IGNORE_NRQ,'emsi_options'))) || $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', )); // Add unknown FTNs to the DB $so = ($this->node->aka_remote_authed->count()) ? $this->node->aka_remote_authed->first()->system : System::createUnknownSystem(); if ($so && $so->exists) { foreach ($this->node->aka_other as $aka) if (! Address::findFTN($aka)) { $oo = Address::createFTN($aka,$so); $oo->validated = TRUE; $oo->save(); } // Log session in DB $slo = new SystemLog; $slo->items_sent = $this->send->total_sent; $slo->items_sent_size = $this->send->total_sent_bytes; $slo->items_recv = $this->recv->total_recv; $slo->items_recv_size = $this->recv->total_recv_bytes; $slo->mailer_id = $mo->id; $slo->sessiontime = $this->node->session_time; $slo->result = ($rc & self::S_MASK); $slo->originate = $this->originate; $so->logs()->save($slo); // If we are autohold, then remove that if ($so->autohold) { $so->autohold = FALSE; $so->save(); } } // @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; } /* SE_* flags determine our session processing status, at any point in time */ /** * Clear a session bit (SE_*) * * @param int $key */ protected function sessionClear(int $key): void { $this->session &= ~$key; } /** * Get a session bit (SE_*) * * @param int $key * @return bool */ protected function sessionGet(int $key): bool { return ($this->session & $key); } /** * Set a session bit (SE_*) * * @param int $key */ protected function sessionSet(int $key): void { $this->session |= $key; } }