<?php namespace App\Classes\Protocol; use Carbon\Carbon; use Illuminate\Support\Facades\Log; use App\Classes\Protocol as BaseProtocol; use App\Classes\Sock\Exception\SocketException; use App\Classes\Sock\SocketClient; use App\Exceptions\InvalidFTNException; use App\Interfaces\CRC as CRCInterface; use App\Interfaces\Zmodem as ZmodemInterface; use App\Models\{Address,Mailer,Setup}; 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 = "\x0d"; // \r private const NL = "\x0a"; // \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 */ public function onConnect(SocketClient $client): ?int { // If our parent returns a PID, we've forked if (! parent::onConnect($client)) { Log::withContext(['pid'=>getmypid()]); $this->session(Mailer::where('name','EMSI')->singleOrFail(),$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 & files size (bytes) $makedata .= sprintf('{TRAF}{%lX %lX}',$this->send->mail_size,$this->send->files_size); // MOH# - Mail On Hold - bytes waiting $makedata .= sprintf('{MOH#}{[%lX]}',$this->send->total_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); // FREQ Time - NRQ (No Requests if this is not defined) $makedata .= sprintf('{OHFR}{Always! CM}'); $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 (static::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,TRUE))) { Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka)); $this->node->ftn_other = $rem_aka; continue; // If we only present limited AKAs dont validate password against akas outside of the domains we present } elseif (is_null(our_address($o))) { Log::alert(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(','))); $this->node->ftn_other = $rem_aka; continue; } elseif (! $o->active) { Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka)); $this->node->ftn_other = $rem_aka; continue; } else { Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka)); } } catch (InvalidFTNException $e) { Log::error(sprintf('%s:! AKA is INVALID [%s] (%s), ignoring',self::LOGKEY,$rem_aka,$e->getMessage())); continue; } catch (\Exception) { Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka)); return self::S_FAILURE|self::S_ADDTRY; } // 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 receive handshake',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::error(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 transmit Handshake',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 (static::DEBUG) Log::debug(sprintf('%s:+ Starting EMSI 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 (static::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(); if ($this->down) { Log::info(sprintf('%s:! System down for maintenance',self::LOGKEY)); $this->client->buffer_add(self::EMSI_NAK.'Sorry down for maintenance, call back again after a few minutes'.self::CR.self::CR); $this->client->buffer_flush(5); return -1; } $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 (static::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(bool $force_queue=FALSE): 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)); $this->force_queue = $force_queue; $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:+ Start WAZOO Receive',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,$this->force_queue); 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) { if (! $ao->validated) { Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn)); continue; } // Send mail while ($this->send->mail($ao)) { $z = new Zmodem; if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->togo_count) $z->zmodem_sendfile($this->send,$this->node); } // Send files while ($this->send->files($ao)) { $z = new Zmodem; if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->togo_count) $z->zmodem_sendfile($this->send,$this->node); } } Log::debug(sprintf('%s:- Finished sending',self::LOGKEY)); return (($z && $z->zmodem_senddone())<0); } }