1227 lines
34 KiB
PHP
1227 lines
34 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\{Address,Setup};
|
|
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
|
|
{
|
|
private const LOGKEY = 'PE-';
|
|
|
|
use CRCTrait;
|
|
|
|
/* CONSTS */
|
|
|
|
public const PORT = 60179;
|
|
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";
|
|
|
|
/* FEATURES */
|
|
|
|
/** Ignore NRQ */
|
|
public const F_IGNORE_NRQ = 1<<0;
|
|
/** Send an immediate EMSI_INQ on connect */
|
|
public const F_DO_PREVENT = 1<<1;
|
|
|
|
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;
|
|
|
|
protected const MO_CHAT = 4;
|
|
|
|
// 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)) {
|
|
Log::withContext(['pid'=>getmypid()]);
|
|
|
|
// @todo Can this be SESSION_EMSI? if so, set an object class value that in EMSI of SESSION_EMSI, and move this method to the parent class
|
|
$this->session(self::SESSION_AUTO,$client,(new Address));
|
|
$this->client->close();
|
|
exit(0);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Send our welcome banner
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
private function emsi_banner(): void
|
|
{
|
|
Log::debug(sprintf('%s:+ Showing EMSI banner',self::LOGKEY));
|
|
|
|
$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
|
|
* @throws Exception
|
|
*/
|
|
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
|
|
$makedata .= sprintf('{EMSI}{%s}{%s}{%s}{%s}',
|
|
$this->our_addresses()->pluck('ftn')->join(' '),
|
|
($this->node->password === '-') ? '' : $this->node->password,
|
|
join(',',$link_codes),
|
|
join(',',$compat_codes),
|
|
);
|
|
|
|
// Mailer Details
|
|
$makedata .= sprintf('{%s}{%s}{%s}{%s}',
|
|
Setup::product_id(),
|
|
config('app.name'),
|
|
$this->setup->version,
|
|
'#000000' // Serial Numbers
|
|
);
|
|
|
|
// System Details
|
|
$makedata .= sprintf('{IDENT}{[%s][%s][%s][%s][%d][%s]}',
|
|
$this->setup->system->name,
|
|
$this->setup->system->location,
|
|
$this->setup->system->sysop,
|
|
$this->setup->system->phone ?: '-Unpublished-',
|
|
self::TCP_SPEED,
|
|
'XA' // Nodelist Flags
|
|
);
|
|
|
|
// TRAF - netmail/echomail traffic size (bytes)
|
|
$makedata .= sprintf('{TRAF}{%lX %lX}',$this->send->mail_size,$this->send->size);
|
|
|
|
// MOH# - Mail On Hold - bytes waiting
|
|
$makedata .= sprintf('{MOH#}{[%lX]}',$this->send->mail_size);
|
|
|
|
// EMD5 - MD5 unique string
|
|
|
|
// Transaction Number (Time in local time)
|
|
$makedata .= sprintf('{TRX#}{[%lX]}',Carbon::now()->timestamp+Carbon::now($this->node->node_timezone)->offset);
|
|
|
|
$makedata .= sprintf('{TZUTC}{[%+05d]}',-10*60);
|
|
|
|
// @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',crc16(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
|
|
{
|
|
if ($this->DEBUG)
|
|
Log::debug(sprintf('%s:+ emsi_parsedat',self::LOGKEY));
|
|
|
|
$l = 0;
|
|
|
|
if (! ($str=strstr($str,self::EMSI_DAT))) {
|
|
Log::error(sprintf('%s:! No EMSI_DAT signature found?',self::LOGKEY));
|
|
|
|
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]!',self::LOGKEY,$l,$x));
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Check the CRC16 checksum
|
|
sscanf(substr($str,strlen($str)-4),"%04X",$l);
|
|
|
|
/* Bad EMSI CRC */
|
|
if ($l != ($x = crc16(substr($str,2,strlen($str)-6)))) {
|
|
Log::error(sprintf('%s:! Bad EMSI_DAT CRC: [%04X], should be: [%04X]!',self::LOGKEY,$l,$x));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* No EMSI ident */
|
|
if (strncmp(substr($str,14),"{EMSI}",6)) {
|
|
Log::error(sprintf('%s:! No EMSI fingerprint?',self::LOGKEY));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* {AKAs} */
|
|
$x = 21;
|
|
foreach (explode(' ',$this->emsi_dat_parse($str,$x)) as $rem_aka) {
|
|
Log::debug(sprintf('%s: - Parsing AKA [%s]',self::LOGKEY,$rem_aka));
|
|
|
|
try {
|
|
if (! ($o = Address::findFTN($rem_aka))) {
|
|
Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
|
|
|
|
$this->node->ftn_other = $rem_aka;
|
|
continue;
|
|
}
|
|
|
|
} catch (Exception) {
|
|
Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka));
|
|
|
|
continue;
|
|
}
|
|
|
|
// Check if the remote has our AKA
|
|
if ($this->setup->system->addresses->pluck('ftn')->search($o->ftn) !== FALSE) {
|
|
Log::error(sprintf('%s: ! AKA is OURS [%s]',self::LOGKEY,$o->ftn));
|
|
|
|
continue;
|
|
}
|
|
|
|
// @todo lock nodes
|
|
Log::info(sprintf('%s: - Remote AKA [%s]',self::LOGKEY,$o->ftn));
|
|
$this->node->ftn = $o;
|
|
}
|
|
|
|
if ($this->originate AND ! $this->node->originate_check()) {
|
|
Log::error(sprintf('%s: ! We didnt get who we called?',self::LOGKEY));
|
|
|
|
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, and we expect [%s]',self::LOGKEY,$p,$this->node->password));
|
|
|
|
if ($p || $this->node->password)
|
|
$this->node->optionSet(self::O_BAD);
|
|
|
|
} else {
|
|
$this->node->optionSet(self::O_PWD);
|
|
Log::info(sprintf('%s: - Remote Authed [%d] AKAs',self::LOGKEY,$c));
|
|
}
|
|
|
|
/* Link codes */
|
|
Log::notice(sprintf('%s: - Remote Link Codes [%s]',self::LOGKEY,$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)',self::LOGKEY));
|
|
break;
|
|
|
|
case 'NRQ':
|
|
Log::debug(sprintf('%s: = No file requests accepted by this system (NRQ)',self::LOGKEY));
|
|
$this->node->optionSet(self::O_NRQ);
|
|
break;
|
|
|
|
case 'XMA':
|
|
Log::debug(sprintf('%s: = Node supports other forms of compressed mail (XMA)',self::LOGKEY));
|
|
break;
|
|
|
|
case 'ZAP':
|
|
Log::debug(sprintf('%s: = Remote wants ZEDZAP',self::LOGKEY));
|
|
$this->node->optionSet(self::P_ZEDZAP);
|
|
break;
|
|
|
|
case 'ZMO':
|
|
Log::debug(sprintf('%s: = Remote wants ZMODEM',self::LOGKEY));
|
|
$this->node->optionSet(self::P_ZMODEM);
|
|
break;
|
|
|
|
default:
|
|
Log::info(sprintf('%s: = Ignoring unknown option: [%s] ',self::LOGKEY,$code));
|
|
}
|
|
}
|
|
|
|
/* Mailer code */
|
|
Log::notice(sprintf('%s: - Remote Mailer Code [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x))); // hex
|
|
|
|
/* Mailer name */
|
|
Log::notice(sprintf('%s: - Remote Mailer [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));
|
|
|
|
/* Mailer version */
|
|
Log::notice(sprintf('%s: - Remote Mailer Version [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));
|
|
|
|
/* Mailer serial number */
|
|
Log::notice(sprintf('%s: - Remote Mailer Serial Number [%s]',self::LOGKEY,$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]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
|
|
/* System location */
|
|
Log::notice(sprintf('%s: - Remote Location [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
|
|
/* Operator name */
|
|
Log::notice(sprintf('%s: - Remote Operator [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
|
|
/* Phone */
|
|
Log::notice(sprintf('%s: - Remote Phone Number [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
|
|
/* Baud rate */
|
|
$this->client->speed = $this->emsi_dat_parse($str,$x,']');
|
|
|
|
/* Flags */
|
|
Log::notice(sprintf('%s: - Remote Flags [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
$x++;
|
|
|
|
break;
|
|
|
|
// {TRAF}{}
|
|
case 'TRAF':
|
|
$x = $t+1;
|
|
Log::notice(sprintf('%s: - Remote TRAF [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));
|
|
|
|
break;
|
|
|
|
// {OHFR}{}
|
|
case 'OHFR':
|
|
$x = $t+1;
|
|
Log::notice(sprintf('%s: - Remote OHFR [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));
|
|
|
|
break;
|
|
|
|
// {MOH#}{[]}
|
|
case 'MOH#':
|
|
$x = $t+2;
|
|
Log::notice(sprintf('%s: - Remote MOH# [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
$x++;
|
|
|
|
break;
|
|
|
|
// {TRX#}{[]}
|
|
case 'TRX#':
|
|
$x = $t+2;
|
|
Log::notice(sprintf('%s: - Remote TRX [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
$x++;
|
|
|
|
break;
|
|
|
|
// {TZUTC}{[]}
|
|
case 'TZUTC':
|
|
$x = $t+2;
|
|
Log::notice(sprintf('%s: - Remote TZUTC [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
|
|
$x++;
|
|
|
|
break;
|
|
|
|
default:
|
|
$x = $t+1;
|
|
Log::notice(sprintf('%s: - Remote UNKNOWN [%s] (%s)',self::LOGKEY,$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:+ emsi_recv',self::LOGKEY));
|
|
|
|
Log::debug(sprintf('%s: - STEP 1',self::LOGKEY));
|
|
/*
|
|
* 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',self::LOGKEY));
|
|
/* 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',self::LOGKEY));
|
|
/* Step 3
|
|
* +-+------------------------------------------------------------------+
|
|
* :3: T1=20 seconds :
|
|
* +-+------------------------------------------------------------------+
|
|
*/
|
|
|
|
$t1 = $this->client->timer_set(20);
|
|
|
|
step4:
|
|
Log::debug(sprintf('%s: - STEP 4',self::LOGKEY));
|
|
/* 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',self::LOGKEY));
|
|
|
|
goto step3;
|
|
}
|
|
|
|
if (! strncmp($p,self::EMSI_DAT,10)) {
|
|
Log::debug(sprintf('%s:- Received EMSI_DAT',self::LOGKEY));
|
|
|
|
Log::debug(sprintf('%s: - STEP 5',self::LOGKEY));
|
|
/* 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',self::LOGKEY));
|
|
/* Step 6
|
|
+-+------------------------------------------------------------------+
|
|
:6: Received EMSI_DAT packet OK, exit. :
|
|
+-+------------------------------------------------------------------+
|
|
*/
|
|
|
|
return self::OK;
|
|
|
|
} else {
|
|
Log::debug(sprintf('%s: - EMSI_DAT didnt parse',self::LOGKEY));
|
|
|
|
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.',self::LOGKEY));
|
|
|
|
$rew = strstr($p,self::EMSI_BEG,TRUE);
|
|
if ($rew && $rew != $p) {
|
|
Log::notice(sprintf('%s: - Got EMSI_DAT at offset [%d].',self::LOGKEY,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:+ EMSI send',self::LOGKEY));
|
|
|
|
Log::debug(sprintf('%s: - STEP 1',self::LOGKEY));
|
|
/* 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',self::LOGKEY));
|
|
/* 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',self::LOGKEY));
|
|
$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',self::LOGKEY));
|
|
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;
|
|
|
|
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?',self::LOGKEY));
|
|
$this->client->buffer_add(self::EMSI_ACK);
|
|
$this->client->buffer_add(self::EMSI_ACK);
|
|
$this->client->buffer_flush(1);
|
|
|
|
$t2 = $this->client->timer_set($this->client->timer_rest($t2) >> 2);
|
|
|
|
} else if (! strncmp($p,self::EMSI_REQ,self::EMSI_SEQ_LEN)) {
|
|
Log::notice(sprintf('%s: - Got EMSI_REQ - skipping...',self::LOGKEY),['p'=>$p]);
|
|
|
|
} else if (! strncmp($p,self::EMSI_ACK,self::EMSI_SEQ_LEN)) {
|
|
Log::debug(sprintf('%s: - Got EMSI_ACK',self::LOGKEY));
|
|
|
|
Log::debug(sprintf('%s: - STEP 5',self::LOGKEY));
|
|
/* 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',self::LOGKEY));
|
|
}
|
|
}
|
|
} /* 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:+ protocol_init',self::LOGKEY));
|
|
|
|
$got = 0;
|
|
$tries = 0;
|
|
$p = '';
|
|
|
|
if ($this->originate) {
|
|
$gotreq = 0;
|
|
|
|
if ($this->setup->optionGet(EMSI::F_DO_PREVENT,'emsi_options'))
|
|
$this->capSet(EMSI::F_DO_PREVENT,self::O_YES);
|
|
|
|
// 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)',self::LOGKEY,$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->capGet(EMSI::F_DO_PREVENT,self::O_YES) && $tries === 0) {
|
|
$this->capSet(EMSI::F_DO_PREVENT,self::O_NO);
|
|
|
|
$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)...',self::LOGKEY,$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',self::LOGKEY));
|
|
if ($gotreq++)
|
|
return self::OK;
|
|
|
|
$this->client->buffer_add(self::EMSI_INQ.self::CR);
|
|
$this->client->buffer_flush(5);
|
|
|
|
} elseif ($p && strstr($p,self::EMSI_BEG) && strstr($p,self::EMSI_ARGUS1)) {
|
|
Log::info(sprintf('%s: - Got Intro [%s]',self::LOGKEY,$p));
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($ch > 31)
|
|
$p .= chr($ch);
|
|
|
|
if (strlen($p) >= self::EMSI_BUF)
|
|
return self::ERROR;
|
|
}
|
|
}
|
|
|
|
$this->client->rx_purge();
|
|
$this->client->tx_purge();
|
|
$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)',self::LOGKEY,$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',self::LOGKEY));
|
|
|
|
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
|
|
{
|
|
// @todo introduce emsi_init() to perform the job of protocol_init. Only needs to be done when we originate a session
|
|
|
|
Log::debug(sprintf('%s:+ Starting EMSI Protocol Session',self::LOGKEY));
|
|
|
|
$was_req = 0;
|
|
$got_req = 0;
|
|
|
|
// Outbound session
|
|
if ($this->originate) {
|
|
Log::debug(sprintf('%s: - Outbound session',self::LOGKEY));
|
|
$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]',self::LOGKEY,$this->client->address_remote));
|
|
|
|
// Inbound session
|
|
} else {
|
|
Log::debug(sprintf('%s: - Inbound session',self::LOGKEY));
|
|
$rc = $this->emsi_recv(self::SM_INBOUND);
|
|
|
|
if ($rc < 0) {
|
|
Log::error(sprintf('%s:! Unable to establish EMSI session',self::LOGKEY));
|
|
|
|
return (self::S_REDIAL|self::S_ADDTRY);
|
|
}
|
|
|
|
Log::info(sprintf('%s: - Starting inbound EMSI session from [%s]',self::LOGKEY,$this->client->address_remote));
|
|
|
|
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)',self::LOGKEY,$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);
|
|
}
|
|
|
|
// @todo Lock Node AKAs
|
|
|
|
Log::info(sprintf('%s: - We have %lu%s mail, %lu%s files',self::LOGKEY,$this->send->mail_size,'b',$this->send->files_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',self::LOGKEY));
|
|
|
|
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]',self::LOGKEY,$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]',self::LOGKEY,$t));
|
|
|
|
Log::debug(sprintf('%s: = Options: %s%s%s%s%s%s%s%s%s%s%s',
|
|
self::LOGKEY,$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:+ wazoorecv',self::LOGKEY));
|
|
|
|
// @todo If the node is not defined in the DB node->address is NULL. Need to figure out how to handle those nodes.
|
|
$rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address);
|
|
|
|
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:+ wazoosend [%d]',self::LOGKEY,$zap));
|
|
|
|
$z = NULL;
|
|
|
|
// See if there is anything to add to the outbound
|
|
// Add our mail to the queue if we have authenticated
|
|
if ($this->node->aka_authed)
|
|
foreach ($this->node->aka_remote_authed as $ao) {
|
|
// Send mail
|
|
while ($this->send->mail($ao)) {
|
|
$z = new Zmodem;
|
|
|
|
if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->total_count)
|
|
$z->zmodem_sendfile($this->send);
|
|
}
|
|
|
|
// Send files
|
|
while ($this->send->files($ao)) {
|
|
$z = new Zmodem;
|
|
|
|
if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->total_count)
|
|
$z->zmodem_sendfile($this->send);
|
|
}
|
|
}
|
|
|
|
Log::debug(sprintf('%s:- Finished sending',self::LOGKEY));
|
|
return (($z && $z->zmodem_senddone())<0);
|
|
}
|
|
} |