528 lines
14 KiB
PHP
528 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()]);
|
|
|
|
$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;
|
|
}
|
|
} |