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 Address)); $this->client->close(); Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->address_remote)); exit(0); } 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']; // Only show our AKAs relevant to the site we are ccmmunicating with if ($this->setup->optionGet(Setup::O_HIDEAKA)) { $addresses = collect(); foreach ($this->node->aka_remote as $ao) $addresses = $addresses->merge($this->setup->system->match($ao->zone)); $addresses = $addresses->unique(); Log::debug(sprintf('%s: - Presenting limited AKAs [%s]',__METHOD__,$addresses->pluck('ftn')->join(','))); } else { $addresses = $this->setup->system->addresses; Log::debug(sprintf('%s: - Presenting ALL our AKAs [%s]',__METHOD__,$addresses->pluck('ftn')->join(','))); } // Site address, password and compatibility $makedata .= sprintf('{EMSI}{%s}{%s}{%s}{%s}', $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][-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',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 { Log::debug(sprintf('%s: + Start',__METHOD__)); $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 = crc16(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 = Address::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->system->addresses->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, and we expect [%s]',__METHOD__,$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',__METHOD__,$c)); } /* 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->speed = $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; 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?',__METHOD__)); $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...',__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',__METHOD__)); 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]',__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',__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->address_remote)); // 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->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)',__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); } // @todo Lock Node AKAs 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__)); // @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: + Start [%d]',__METHOD__,$zap)); // 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) { $this->send->mail($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',__METHOD__)); return ($z->zmodem_senddone()<0); } }