clrghouz/app/Classes/Protocol/EMSI.php

1187 lines
33 KiB
PHP

<?php
namespace App\Classes\Protocol;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\Node;
use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface;
use App\Traits\CRC as CRCTrait;
// http://ftsc.org/docs/fsc-0056.001
// http://ftsc.org/docs/fsc-0088.001
final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
{
use CRCTrait;
private const EMSI_BEG = '**EMSI_';
private const EMSI_ARGUS1 = '-PZT8AF6-';
private const EMSI_DAT = self::EMSI_BEG.'DAT';
private const EMSI_REQ = self::EMSI_BEG.'REQA77E';
private const EMSI_INQ = self::EMSI_BEG.'INQC816';
private const EMSI_ACK = self::EMSI_BEG.'ACKA490';
private const EMSI_NAK = self::EMSI_BEG.'NAKEEC3';
private const EMSI_HBT = self::EMSI_BEG.'HBTEAEE';
private const CR = "\r";
private const NL = "\n";
private const DEL = "\x08";
private const EMSI_BUF = 8192;
private const TMP_LEN = 1024;
private const SM_INBOUND = 0;
private const SM_OUTBOUND = 1;
private const EMSI_HSTIMEOUT = 60; /* Handshake timeout */
private const EMSI_SEQ_LEN = 14;
private const EMSI_LOG_IN = 0;
private const EMSI_LOG_OUT = 1;
/* FREQs flags */
private const FR_NOTHANDLED = (-1);
private const FR_NOTAVAILABLE = 0;
private const FR_AVAILABLE = 1;
private const EMSI_RESEND_TO = 5;
// Our session status
private int $session;
// Protocols we support in order
// @todo This should be a config item
private array $protocols = [
//'4'=>self::P_HYDRA4,
//'8'=>self::P_HYDRA8,
//'6'=>self::P_HYDRA16,
//'H'=>self::P_HYDRA,
//'J'=>self::P_JANUS,
//'D'=>self::P_DIRZAP,
//'Z'=>self::P_ZEDZAP,
'1'=>self::P_ZMODEM,
];
/**
* Incoming EMSI session
*
* @param SocketClient $client
* @return int|null
* @throws SocketException
* @throws Exception
*/
public function onConnect(SocketClient $client): ?int
{
// If our parent returns a PID, we've forked
if (! parent::onConnect($client)) {
$this->session(self::SESSION_AUTO,$client,(new Node));
$this->client->close();
Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->getAddress()));
}
return NULL;
}
/**
* Send our welcome banner
*
* @throws Exception
*/
private function emsi_banner(): void
{
Log::debug(sprintf('%s: + Start',__METHOD__));
$banner = 'This is a mail only system - unless you are a mailer, you should disconnect :)';
$this->client->buffer_add(self::EMSI_REQ.str_repeat(self::DEL,strlen(self::EMSI_REQ)).$banner.self::CR);
$this->client->buffer_flush(5);
}
/**
* Create the EMSI_DAT
*
* @return string
*/
private function emsi_makedat(): string
{
$makedata = sprintf('%s0000',self::EMSI_DAT);
/*
* Link Codes
*
* Link codes is a string of flags that specify desired connect conditions. These codes are separated by commas.
* New codes may be added with prior approval from the author of this document.
*
* Calling system options:
* PUA Pickup mail for all presented addresses.
* PUP Pickup mail for primary address only.
* NPU No mail pickup desired.
*
* Answering system options:
* HAT Hold ALL traffic.
* HXT Hold compressed mail traffic.
* HRQ Hold file requests (not processed at this time).
*/
$link_codes = $this->originate ? ['8N1','PUA'] : ['8N1'];
/*
* Compatibility codes
*
* The calling system must list supported protocols first and descending order of preference (the most desirable
* protocol should be listed first). The answering system should only present one protocol and it should be the
* first item in the compatibility_codes field.
*
* Protocols
-----------------------------------------------------------------
DZA* DirectZAP (Zmodem variant, reduced escape set).
TZA DirectZap (TrapDoor DirectZap varient)
ZAP ZedZap (Zmodem variant, upe 8K blocks).
ZMO** Zmodem w/1,024 packets (Wazoo ZedZip)
JAN Janus bi-directional.
KER Kermit.
HYD Hydra bi-directional (link flags define parameters)
SLK SeaLink (no TYSNC, No MDM7, No TeLink)
CHT Chat?
Other codes
-----------------------------------------------------------------
NCP No compatible protocols (failure).
NRQ No file requests accepted by this system. (IE: capability not implemented)
FRQ Node accepts and will process file rquests.
ARC ARCmail 0.60-capable, as defined by the FTSC.
XMA Supports other forms of compressed mail.
FNC Filename conversion. This indicates that any transmitted
files must follow the MS-DOS restrictions of an eight
character file name followed by a three character
extension; eg. FILENAME.EXT
DFB Indicates that the system presenting is capabable of fall-back to
FTS1/WAZOO negotiation in the case of failure of EMSI handshake or no
common protocol.
Link Session options:
-----------------------------------------------------------------
RMA Indicates that the presenting site is able to send and process multiple
file requests. If both sites present this flag, the caller will send
any REQ files found for each AKA presented by the answering system.
The answering system will process each received REQ.
PMO PickUp Mail (ARCmail and Packets) ONLY
NFE No TIC'S, associated files or files attachs desired
NXP No compressed mail pickup desired
NRQ File requests not accepted by caller
This flag is presented if file request processing
is disabled TEMPORARILY for any reason
*/
// @todo We need to evaluate what the remote presents
$compat_codes = $this->originate ? ['ZMO','ARC','XMA'] : ['ZMO'];
// Site address, password and compatibility
// @todo Only show the AKAs that is relevant to the node we are connecting to
$makedata .= sprintf('{EMSI}{%s}{%s}{%s}{%s}',
join(' ',$this->setup->nodes->pluck('ftn')->toArray()),
$this->node->password == '-' ? '' : $this->node->password,
join(',',$link_codes),
join(',',$compat_codes),
);
// Mailer Details
$makedata .= sprintf('{%s}{%s}{%s}{%s}',
self::product_code,
config('app.name'),
$this->setup->version,
'#000000' // Serial Numbers
);
// System Details
$makedata .= sprintf('{IDENT}{[%s][%s][%s][-Unpublished-][38400][%s]}',
$this->setup->system_name,
$this->setup->location,
$this->setup->sysop,
'XA' // Nodelist Flags
);
$makedata .= sprintf('{TRAF}{%lX %lX}',$this->send->mail_size,$this->send->file_size);
// @todo Not sure what MOH is for
//$makedata .= sprintf('{MOH#}{[%lX]}',0);
$makedata .= sprintf('{TRX#}{[%lX]}',Carbon::now()->timestamp);
$makedata .= sprintf('{TZUTC}{[%+03d%02d]}',10,0);
// @todo Not sure what OHFR is for
//$makedata .= sprintf('{OHFR}{%s}','Never Never');
/* Calculate emsi length */
$makedata = preg_replace('/0000/',sprintf('%04X',strlen($makedata)-14),$makedata,1);
/* EMSI crc16 */
$makedata .= sprintf('%04X',$this->CRC16USD(substr($makedata,2)));
return $makedata;
}
/**
* Parse the EMSI dat string and return chunks
*
* @param string $str
* @param int $x
* @param string $needle
* @return string
*/
private function emsi_dat_parse(string $str,int &$x,string $needle='}'): string
{
$y = $x;
$t = strpos($str,$needle,$x);
$x = $t+2;
return substr($str,$y,$t-$y);
}
/**
* Parse the received EMSI_DAT for remote system details
*
* @param string $str
* @return int
* @throws Exception
*/
private function emsi_parsedat(string $str): int
{
Log::debug(sprintf('%s: + Start',__METHOD__));
if ($this->DEBUG)
Log::debug(sprintf('%s: - Parsing [%s]',__METHOD__,$str));
$l = 0;
if (! ($str=strstr($str,self::EMSI_DAT))) {
Log::error(sprintf('%s: ! No EMSI_DAT signature found?',__METHOD__));
return 0;
}
// Get our EMSI_DAT length
sscanf(substr($str,10),"%04X",$l);
/* Bad EMSI length */
if ($l != ($x=strlen($str)-18)) {
Log::error(sprintf('%s: ! Bad EMSI_DAT length: [%u], should be: [%u]!',__METHOD__,$l,$x));
return 0;
}
// Check the CRC16 checksum
sscanf(substr($str,strlen($str)-4),"%04X",$l);
/* Bad EMSI CRC */
if ($l != ($x = $this->CRC16USD(substr($str,2,strlen($str)-6)))) {
Log::error(sprintf('%s: ! Bad EMSI_DAT CRC: [%04X], should be: [%04X]!',__METHOD__,$l,$x));
return 0;
}
/* No EMSI ident */
if (strncmp(substr($str,14),"{EMSI}",6)) {
Log::error(sprintf('%s: ! No EMSI fingerprint?',__METHOD__));
return 0;
}
/* {AKAs} */
$x = 21;
foreach (explode(' ',$this->emsi_dat_parse($str,$x)) as $rem_aka) {
Log::debug(sprintf('%s: - Parsing AKA [%s]',__METHOD__,$rem_aka));
try {
if (! ($o = Node::findFTN($rem_aka))) {
Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',__METHOD__,$rem_aka));
continue;
}
} catch (Exception) {
Log::error(sprintf('%s: ! AKA is INVALID [%s]',__METHOD__,$rem_aka));
continue;
}
// Check if the remote has our AKA
if ($this->setup->nodes->pluck('ftn')->search($o->ftn) !== FALSE) {
Log::error(sprintf('%s: ! AKA is OURS [%s]',__METHOD__,$o->ftn));
continue;
}
// @todo lock nodes
Log::info(sprintf('%s: - Remote AKA [%s]',__METHOD__,$o->ftn));
$this->node->ftn = $o;
}
if ($this->originate AND ! $this->node->originate_check()) {
Log::error(sprintf('%s: ! We didnt get who we called?',__METHOD__));
return self::S_FAILURE|self::S_ADDTRY;
}
// By definition, if we are in the DB, we are nodelisted
if ($this->node->aka_num)
$this->node->optionSet(self::O_LST);
/* Password */
$p = $this->emsi_dat_parse($str,$x);
if ($this->originate) {
$c = ($p === $this->node->password);
} else {
$c = $this->node->auth($p);
}
if (! $c) {
Log::info(sprintf('%s: - Remote has password [%s] on us',__METHOD__,$p));
if ($p)
$this->node->optionSet(self::O_BAD);
} else {
$this->node->optionSet(self::O_PWD);
Log::info(sprintf('%s: - Remote Authed [%d] AKAs',__METHOD__,$c));
$this->send->add('/tmp/aa');
}
/* Link codes */
Log::notice(sprintf('%s: - Remote Link Codes [%s]',__METHOD__,$this->emsi_dat_parse($str,$x)));
/* Compatibility codes */
$codes = $this->emsi_dat_parse($str,$x);
if ($codes)
foreach (explode(',',$codes) as $code) {
switch ($code) {
case 'ARC':
Log::debug(sprintf('%s: = Node accepts ARC mail bundle (ARC)',__METHOD__));
break;
case 'NRQ':
Log::debug(sprintf('%s: = No file requests accepted by this system (NRQ)',__METHOD__));
$this->node->optionSet(self::O_NRQ);
break;
case 'XMA':
Log::debug(sprintf('%s: = Node supports other forms of compressed mail (XMA)',__METHOD__));
break;
case 'ZAP':
Log::debug(sprintf('%s: = Remote wants ZEDZAP',__METHOD__));
$this->node->optionSet(self::P_ZEDZAP);
break;
case 'ZMO':
Log::debug(sprintf('%s: = Remote wants ZMODEM',__METHOD__));
$this->node->optionSet(self::P_ZMODEM);
break;
default:
Log::info(sprintf('%s: = Ignoring unknown option: [%s] ',__METHOD__,$code));
}
}
/* Mailer code */
Log::notice(sprintf('%s: - Remote Mailer Code [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); // hex
/* Mailer name */
Log::notice(sprintf('%s: - Remote Mailer [%s]',__METHOD__,$this->emsi_dat_parse($str,$x)));
/* Mailer version */
Log::notice(sprintf('%s: - Remote Mailer Version [%s]',__METHOD__,$this->emsi_dat_parse($str,$x)));
/* Mailer serial number */
Log::notice(sprintf('%s: - Remote Mailer Serial Number [%s]',__METHOD__,$this->emsi_dat_parse($str,$x)));
while ($t=strpos($str,'}',$x)) {
$p = substr($str,$x,$t-$x);
$t++; // End of this field
switch ($p) {
// {IDENT}{[]}
case 'IDENT':
/* System name */
$x = $t+2;
Log::notice(sprintf('%s: - Remote System [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
/* System location */
Log::notice(sprintf('%s: - Remote Location [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
/* Operator name */
Log::notice(sprintf('%s: - Remote Operator [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
/* Phone */
Log::notice(sprintf('%s: - Remote Phone Number [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
/* Baud rate */
$this->client->setSpeed($this->emsi_dat_parse($str,$x,']'));
/* Flags */
Log::notice(sprintf('%s: - Remote Flags [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
$x++;
break;
// {TRAF}{}
case 'TRAF':
$x = $t+1;
Log::notice(sprintf('%s: - Remote TRAF [%s]',__METHOD__,$this->emsi_dat_parse($str,$x)));
break;
// {OHFR}{}
case 'OHFR':
$x = $t+1;
Log::notice(sprintf('%s: - Remote OHFR [%s]',__METHOD__,$this->emsi_dat_parse($str,$x)));
break;
// {MOH#}{[]}
case 'MOH#':
$x = $t+2;
Log::notice(sprintf('%s: - Remote MOH# [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
$x++;
break;
// {TRX#}{[]}
case 'TRX#':
$x = $t+2;
Log::notice(sprintf('%s: - Remote TRX [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
$x++;
break;
// {TZUTC}{[]}
case 'TZUTC':
$x = $t+2;
Log::notice(sprintf('%s: - Remote TZUTC [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']')));
$x++;
break;
default:
$x = $t+1;
Log::notice(sprintf('%s: - Remote UNKNOWN [%s] (%s)',__METHOD__,$this->emsi_dat_parse($str,$x),$p));
}
}
return 1;
}
/**
* STEP 2A, RECEIVE EMSI HANDSHAKE
*
* @throws SocketException
* @throws Exception
*/
private function emsi_recv(int $mode): int
{
Log::debug(sprintf('%s: + Start',__METHOD__));
Log::debug(sprintf('%s: - STEP 1',__METHOD__));
/*
* Step 1
* +-+------------------------------------------------------------------+
* :1: Tries=0, T1=20 seconds, T2=60 seconds :
* +-+------------------------------------------------------------------+
*/
$p = '';
$tries = 0;
$t1 = $this->client->timer_set(20);
$t2 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
do {
step2:
Log::debug(sprintf('%s: - STEP 2',__METHOD__));
/* Step 2
+-+------------------------------------------------------------------+
:2: Increment Tries :
: : :
: : Tries>6? Terminate, and report failure. :
: +------------------------------------------------------------------+
: : Are we answering system? Transmit EMSI_REQ, go to step 3. :
: +------------------------------------------------------------------+
: : Tries>1? Transmit EMSI_NAK, go to step 3. :
: +------------------------------------------------------------------+
: : Go to step 4. :
+-+------------------------------------------------------------------+
*/
if (++$tries > 6)
return self::TIMEOUT;
if ($mode == self::SM_INBOUND) {
$this->client->buffer_add(self::EMSI_REQ.self::CR);
} elseif ($tries > 1) {
$this->client->buffer_add(self::EMSI_NAK.self::CR);
} else {
goto step4;
}
$this->client->buffer_flush(5);
step3:
Log::debug(sprintf('%s: - STEP 3',__METHOD__));
/* Step 3
* +-+------------------------------------------------------------------+
* :3: T1=20 seconds :
* +-+------------------------------------------------------------------+
*/
$t1 = $this->client->timer_set(20);
step4:
Log::debug(sprintf('%s: - STEP 4',__METHOD__));
/* Step 4
+-+------------------------------------------------------------------+
:4: Wait for EMSI sequence until EMSI_HBT or EMSI_DAT or any of the :
: : timers have expired. :
: : :
: : If T2 has expired, terminate call and report failure. :
: +------------------------------------------------------------------+
: : If T1 has expired, go to step 2. :
: +------------------------------------------------------------------+
: : If EMSI_HBT received, go to step 3. :
: +------------------------------------------------------------------+
: : If EMSI_DAT received, go to step 5. :
: +------------------------------------------------------------------+
: : Go to step 4. :
+-+------------------------------------------------------------------+
*/
$got = 0;
while (TRUE) {
$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
///Log::debug(sprintf('%s: - Got [%x]{%d} (%c)',__METHOD__,$ch,$ch,$ch));
if (($ch != self::TIMEOUT) && ($ch < 0))
return $ch;
if ($this->client->timer_expired($t2))
return self::TIMEOUT;
/* goto step2; */
if ($this->client->timer_expired($t1))
break;
if ($ch == self::TIMEOUT)
continue;
if (! $got) {
if ($ch == ord('*'))
$got = 1;
else
continue;
}
if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) {
if (! strncmp($p,self::EMSI_HBT,self::EMSI_SEQ_LEN)) {
Log::debug(sprintf('%s: - Received EMSI_HBT',__METHOD__));
goto step3;
}
if (! strncmp($p,self::EMSI_DAT,10)) {
Log::debug(sprintf('%s: - Received EMSI_DAT',__METHOD__));
Log::debug(sprintf('%s: - STEP 5',__METHOD__));
/* Step 5
+-+------------------------------------------------------------------+
:5: Receive EMSI_DAT packet :
: +------------------------------------------------------------------+
: : Packet received OK? Transmit EMSI_ACK twice, and :
: : go to step 6. :
: +------------------------------------------------------------------+
: : Go to step 2. :
+-+------------------------------------------------------------------+
*/
$ch = $this->emsi_parsedat($p);
if ($ch) {
$this->client->buffer_add(self::EMSI_ACK.self::CR);
$this->client->buffer_add(self::EMSI_ACK.self::CR);
$this->client->buffer_flush(5);
Log::debug(sprintf('%s: - STEP 6',__METHOD__));
/* Step 6
+-+------------------------------------------------------------------+
:6: Received EMSI_DAT packet OK, exit. :
+-+------------------------------------------------------------------+
*/
return self::OK;
} else {
Log::debug(sprintf('%s: - EMSI_DAT didnt parse',__METHOD__));
goto step2;
}
}
$p = '';// Clear our EMSI dat since the return is the end of a transmission and its not what we want.
goto step4;
} else {
if (strlen($p) >= self::EMSI_BUF) {
Log::warning(sprintf('%s: ! EMSI_DAT packet too long.',__METHOD__));
$rew = strstr($p,self::EMSI_BEG,TRUE);
if ($rew && $rew != $p) {
Log::notice(sprintf('%s: - Got EMSI_DAT at offset [%d].',__METHOD__,strlen($rew)));
$p = substr($p,strlen($rew));
}
}
if ($ch > 31 && $ch <= 255)
$p .= chr($ch);
}
}
} while(! $this->client->timer_expired($t2));
return self::TIMEOUT;
}
/**
* STEP 2B, TRANSMIT EMSI HANDSHAKE
*
* @throws SocketException
* @throws Exception
*/
private function emsi_send(): int
{
Log::debug(sprintf('%s: + Start',__METHOD__));
Log::debug(sprintf('%s: - STEP 1',__METHOD__));
/* Step 1
+-+------------------------------------------------------------------+
:1: Tries=0, T1=60 seconds :
+-+------------------------------------------------------------------+
*/
$p = '';
$tries = 0;
$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
step2:
Log::debug(sprintf('%s: - STEP 2',__METHOD__));
/* Step 2
+-+------------------------------------------------------------------+
:2: Transmit EMSI_DAT packet and increment Tries :
: : :
: +------------------------------------------------------------------+
: : Tries>6? Terminate, and report failure. :
: +------------------------------------------------------------------+
: : Go to step 3. :
+-+------------------------------------------------------------------+
*/
if (++$tries > 6)
return self::TIMEOUT;
$this->client->buffer_add($this->emsi_makedat().self::CR);
$this->client->buffer_flush(5);
/* Step 3
+-+------------------------------------------------------------------+
:3: T2=20 seconds :
+-+------------------------------------------------------------------+
*/
Log::debug(sprintf('%s: - STEP 3',__METHOD__));
$t2 = $this->client->timer_set(20);
/* Step 4
+-+------------------------------------------------------------------+
:4: Wait for EMSI sequence until T1 has expired :
: : :
: : If T1 has expired, terminate call and report failure. :
: +------------------------------------------------------------------+
: : If T2 has expired, go to step 2. :
: +------------------------------------------------------------------+
: : If EMSI_REQ received, go to step 4. :
: +------------------------------------------------------------------+
: : If EMSI_ACK received, go to step 5. :
: +------------------------------------------------------------------+
: : If any other sequence received, go to step 2. :
+-+------------------------------------------------------------------+
*/
Log::debug(sprintf('%s: - STEP 4',__METHOD__));
while(! $this->client->timer_expired($t1)) {
$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
// Log::debug(sprintf('%s: - Got (%x) {%03d} (%c)',__METHOD__,$ch,$ch,$ch));
if (($ch != self::TIMEOUT) && ($ch < 0))
return $ch;
if ($this->client->timer_expired($t2))
goto step2;
if ($this->client->timer_expired($t1))
return self::TIMEOUT;
if ($ch == self::TIMEOUT)
continue;
$ch &= 0x7f;
/*
* @todo D'Bridge doesnt respond to an EMSI_DAT with EMSI_ACK's suffixed with <CR>
*/
if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) {
if (! $p)
continue;
if (! strncmp($p,self::EMSI_DAT,10)) {
Log::warning(sprintf('%s: - Got unexpected EMSI_DAT - Argus/D\'Bridge?',__METHOD__));
/*
* D'Bridge sends EMSI_DAT immediately after EMSI_INQ, so we'll need to exit here to catch it.
* @todo The interaction with DBridge could be improved, because there are many re-transmits before we correctly handshake.
*/
return self::OK;
} else if (! strncmp($p,self::EMSI_REQ,self::EMSI_SEQ_LEN)) {
Log::notice(sprintf('%s: - Got EMSI_REQ - skipping...',__METHOD__));
} else if (! strncmp($p,self::EMSI_ACK,self::EMSI_SEQ_LEN)) {
Log::debug(sprintf('%s: - Got EMSI_ACK',__METHOD__));
Log::debug(sprintf('%s: - STEP 5',__METHOD__));
/* Step 5
+-+------------------------------------------------------------------+
:5: Received EMSI_ACK, exit. :
+-+------------------------------------------------------------------+
*/
return self::OK;
}
$p = '';
continue;
}
/* Put new symbol in buffer */
if ($ch > 31) {
if (strlen($p) < self::TMP_LEN) {
$p .= chr($ch);
} else {
Log::warning(sprintf('%s: ! EMSI packet too long',__METHOD__));
}
}
} /* goto step4; */
return self::TIMEOUT;
}
private function is_freq_available(): int
{
return self::FR_NOTAVAILABLE; // @todo
/*
if (! cfgs(self::CFG_EXTRP ) && ! cfgs(self::CFG_SRIFRP)) {
return self::FR_NOTHANDLED;
}
return ((cfgs(self::CFG_EXTRP) || cfgs(self::CFG_SRIFRP)) && checktimegaps(cfgs(self::CFG_FREQTIME)));
*/
}
/**
* STEP 1, EMSI INIT
*
* @throws SocketException
* @throws Exception
*/
protected function protocol_init(): int
{
if ($this->DEBUG)
Log::debug(sprintf('%s: + Start',__METHOD__));
$got = 0;
$tries = 0;
$p = '';
if ($this->originate) {
$gotreq = 0;
// Send a character to get a response from the remote
$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
do {
$this->client->send(ord(self::CR),1);
} while (! $this->client->hasData(1) && ! $this->client->timer_expired($t1));
if ($this->client->timer_expired($t1))
return self::TIMEOUT;
$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
while (TRUE) {
$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
if ($this->DEBUG)
Log::debug(sprintf('%s: - Got [%x] (%c)',__METHOD__,$ch,$ch));
if (($ch != self::TIMEOUT) && ($ch < 0))
return $ch;
if ($this->client->timer_expired($t1))
return self::TIMEOUT;
if ($this->client->timer_expired($t2)) {
if ($this->setup->do_prevent && $tries == 0) {
$this->setup->do_prevent = 0;
$this->client->buffer_add(self::EMSI_INQ.self::CR);
$this->client->buffer_flush(5);
} else {
if (++$tries > 10)
return self::TIMEOUT;
Log::debug(sprintf('%s: - Sending EMSI_INQ (Try %d of 10)...',__METHOD__,$tries));
$this->client->buffer_add(self::EMSI_INQ.self::CR);
}
$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
continue;
}
if ($ch == self::TIMEOUT)
continue;
$ch &= 0x7f;
if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) {
if (strstr($p,self::EMSI_REQ)) {
Log::info(sprintf('%s: - Got EMSI_REQ (Send INQ then DAT) [%d]',__METHOD__,$gotreq));
$this->client->buffer_add(self::EMSI_INQ);
$this->client->buffer_flush(5);
/*
* For Dbridge, we need to exit here and immediately send our EMSI_DAT.
*/
return self::OK;
} elseif ($p && strstr($p,self::EMSI_BEG) && strstr($p,self::EMSI_ARGUS1)) {
Log::info(sprintf('%s: - Got Intro [%s]',__METHOD__,$p));
}
continue;
}
if ($ch > 31)
$p .= chr($ch);
if (strlen($p) >= self::EMSI_BUF)
return self::ERROR;
}
}
$this->client->rx_purge();
$this->client->buffer_clear();
$this->emsi_banner();
$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
while (! $this->client->timer_expired($t1)) {
$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
if ($this->DEBUG)
Log::debug(sprintf('%s: - Got [%x] (%c)',__METHOD__,$ch,$ch));
if (($ch != self::TIMEOUT) && ($ch < 0))
return $ch;
if ($this->client->timer_expired($t1))
return self::TIMEOUT;
if (($ch == self::TIMEOUT) && $this->client->timer_expired($t2)) {
if (! $got) {
$this->emsi_banner();
$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
} else {
$t2 = $t1;
}
continue;
}
$ch &= 0x7f;
if ((! $got) && ($ch == ord('*')))
$got = 1;
if ($got && (($ch == ord(self::CR)) || ($ch == ord(self::NL)))) {
$got = 0;
if (strstr($p, self::EMSI_INQ)) {
Log::info(sprintf('%s: - Got EMSI_REQ (Returning)',__METHOD__));
return self::OK;
}
} else {
if ($got && ($ch > 31))
$p .= chr($ch);
if (strlen($p) >= self::EMSI_BUF)
return self::ERROR;
}
}
return self::TIMEOUT;
}
/**
* Setup our EMSI session
*
* @return int
* @throws Exception
*/
protected function protocol_session(): int
{
Log::debug(sprintf('%s: + Start',__METHOD__));
$was_req = 0;
$got_req = 0;
// Outbound session
if ($this->originate) {
$this->optionSet(self::O_PUA);
//$emsi_lo |= ($this->is_freq_available() <= self::FR_NOTAVAILABLE ) ? self::O_NRQ : $emsi_lo;
if ($this->emsi_send() < 0)
return (self::S_REDIAL|self::S_ADDTRY);
$rc = $this->emsi_recv(self::SM_OUTBOUND);
if ($rc < 0)
return (self::S_REDIAL|self::S_ADDTRY);
Log::info(sprintf('%s: - Starting outbound EMSI session to [%s]',__METHOD__,$this->client->getAddress()));
// @todo Lock Node AKAs
// Inbound session
} else {
$rc = $this->emsi_recv(self::SM_INBOUND);
if ($rc < 0) {
Log::error(sprintf('%s: ! Unable to establish EMSI session',__METHOD__));
return (self::S_REDIAL|self::S_ADDTRY);
}
Log::info(sprintf('%s: - Starting inbound EMSI session from [%s]',__METHOD__,$this->client->getAddress()));
// @todo Lock Node AKAs
if ($this->node->aka_authed) {
$xproto = $this->is_freq_available();
if ($xproto == self::FR_NOTHANDLED || $xproto == self::FR_NOTAVAILABLE)
$this->node->optionSet(self::O_HRQ);
}
foreach ($this->protocols as $p => $key) {
if ($this->node->optionGet($key)) {
Log::debug(sprintf('%s: - Remote supports [%s] (%x)',__METHOD__,$p,$key));
$this->optionSet($key);
}
}
// Disable chat
//$this->node->optionClear(self::MO_CHAT);
if (! $this->protocols)
$this->optionSet(self::P_NCP);
if ($this->emsi_send() < 0)
return (self::S_REDIAL|self::S_ADDTRY);
}
Log::info(sprintf('%s: - We have %lu%s mail, %lu%s files',__METHOD__,$this->send->mail_size,'b',$this->send->file_size,'b'));
$proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK);
switch ($proto) {
case self::P_NONE:
case self::P_NCP:
Log::error(sprintf('%s: ! No compatible protocols',__METHOD__));
return self::S_FAILURE;
case self::P_ZMODEM:
$t = 'ZModem-1k';
break;
case self::P_ZEDZAP:
$t = 'ZedZap';
break;
case self::P_DIRZAP:
$t = 'DirZap';
break;
case self::P_HYDRA4:
$t = 'Hydra-4k';
break;
case self::P_HYDRA8:
$t = 'Hydra-8k';
break;
case self::P_HYDRA16:
$t = 'Hydra-16k';
break;
case self::P_HYDRA:
$t = 'Hydra';
break;
case self::P_JANUS:
$t = 'Janus';
break;
default:
Log::error(sprintf('%s: ? Unknown Protocol [%s]',__METHOD__,$proto));
$t = 'Unknown';
}
$xproto = ($this->optionGet(self::O_RH1) && ($this->node->optionGet(self::O_RH1)));
$x = (substr($t,1,1) == 'H' && $xproto ) ? 'x' : '';
Log::info(sprintf('%s: = Using [%s]',__METHOD__,$t));
Log::debug(sprintf('%s: = Options: %s%s%s%s%s%s%s%s%s%s%s',
__METHOD__,$x,$t,
($this->node->optionGet(self::O_LST)) ? '/LST' : '',
($this->node->optionGet(self::O_PWD)) ? '/PWD' : '',
($this->node->optionGet(self::O_HXT)) ? '/MO': '',
($this->node->optionGet(self::O_HAT)) ? '/HAT' : '',
($this->node->optionGet(self::O_HRQ)) ? '/HRQ' : '',
($this->node->optionGet(self::O_NRQ)) ? '/NRQ' : '',
($this->node->optionGet(self::O_FNC)) ? '/FNC' : '',
($this->node->optionGet(self::O_BAD)) ? '/BAD' : '',
($this->node->optionGet(self::MO_CHAT)) ? '/CHT' : ''
));
//chatinit($this->rnode->opt & self::MO_CHAT ? proto : -1 );
switch ($proto) {
case self::P_ZEDZAP:
case self::P_DIRZAP:
case self::P_ZMODEM:
$this->client->cps = 1;
$xproto = ($proto&self::P_ZEDZAP) ? self::CZ_ZEDZAP : (($proto&self::P_DIRZAP) ? self::CZ_DIRZAP : self::CZ_ZEDZIP);
if ($this->originate) {
$rc = $this->wazoosend($xproto);
if (! $rc)
$rc = $this->wazoorecv($xproto);
if ($got_req && ! $rc)
$rc = $this->wazoosend($xproto);
} else {
$rc = $this->wazoorecv($xproto|0x0100);
if ($rc)
return self::S_REDIAL;
$rc = $this->wazoosend($xproto);
if ($was_req)
$rc = $this->wazoorecv($xproto);
}
break;
case self::P_HYDRA:
case self::P_HYDRA4:
case self::P_HYDRA8:
case self::P_HYDRA16:
switch ($proto) {
case self::P_HYDRA:
$rc = 1;
break;
case self::P_HYDRA4:
$rc = 2;
break;
case self::P_HYDRA8:
$rc = 4;
break;
case self::P_HYDRA16:
$rc = 8;
break;
default:
$rc = 1;
}
//$rc = hydra($this->originate,$rc,$xproto);
break;
case self::P_JANUS:
//$rc = janus();
break;
default:
return self::S_OK;
}
return $rc ? self::S_REDIAL : self::S_OK;
}
/**
* Receive a file with a transfer protocol
*
* @param int $zap
* @return bool
*/
private function wazoorecv(int $zap): bool
{
Log::debug(sprintf('%s: + Start',__METHOD__));
$rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->setup->inbound);
return ($rc == self::RCDO || $rc == self::ERROR);
}
/**
* Possibly receive something from the remote
*
* @param int $zap
* @return bool
* @throws Exception
*/
private function wazoosend(int $zap): bool
{
Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$zap));
$z = new Zmodem;
if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->total_count)
$z->zmodem_sendfile($this->send);
return ($z->zmodem_senddone()<0);
}
}