clrghouz/app/Classes/Protocol.php

530 lines
14 KiB
PHP

<?php
namespace App\Classes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use App\Classes\File\{Receive,Send};
use App\Classes\Protocol\{Binkp,DNS,EMSI,Zmodem};
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
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;
/** @var bool Is the application down for maintenance */
protected bool $down = FALSE;
/** @var int Our mailer ID for logging purposes */
private int $mailer_id;
private array $comms;
protected bool $force_queue = FALSE;
abstract protected function protocol_init(): int;
abstract protected function protocol_session(bool $force_queue=FALSE): int;
/**
* @param Setup $setup
* @throws \Exception
*/
public function __construct(Setup $setup)
{
$this->setup = $setup;
// Some initialisation details
switch (get_class($this)) {
case Binkp::class:
$this->mailer_id = Mailer::where('name','BINKP')->sole()->id;
break;
case DNS::class:
case Zmodem::class:
break;
case EMSI::class:
$this->mailer_id = Mailer::where('name','EMSI')->sole()->id;
break;
default:
throw new \Exception('not handled'.get_class($this));
}
}
/**
* @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
* @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 our parent returns a PID, we've forked
if ($pid)
Log::info(sprintf('%s:+ New connection from [%s], thread [%d] created',self::LOGKEY,$client->address_remote,$pid));
// This is the new thread
else {
Log::withContext(['pid'=>getmypid()]);
Log::debug(sprintf('%s:* Client session starting',self::LOGKEY));
$this->session($client,(new Address));
}
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
* @throws \Exception
*/
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 = our_address();
Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
}
return $addresses;
}
/**
* Setup a session with a remote client
*
* @param SocketClient $client Socket details of session
* @param Address $o If we have an address, we originated a session to this Address
* @return int
* @throws \Exception
*/
public function session(SocketClient $client,Address $o): 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->client = $client;
// @todo This appears to be a bug in laravel? Need to call app()->isDownForMaintenance() twice?
app()->isDownForMaintenance();
$this->down = app()->isDownForMaintenance();
switch (get_class($this)) {
case EMSI::class:
Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));
$this->optionSet(self::O_TCP);
$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::class:
Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY));
$this->optionSet(self::O_TCP);
$rc = $this->protocol_session($this->originate);
break;
case DNS::class:
return $this->protocol_session();
default:
Log::error(sprintf('%s:! Unsupported session type [%s]',self::LOGKEY,get_class($this)));
return self::S_FAILURE;
}
// @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)
// @todo For disabled zones, we shouldnt refuse to record the address
// @todo If the system hasnt presented an address for a configured period (eg: 30 days) assume it no longer carries it
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 = $this->mailer_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();
}
}
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;
}
}