FTNBase\Packet\FSC45::class, '2+' => FTNBase\Packet\FSC48::class, '2e' => FTNBase\Packet\FSC39::class, '2.0' => FTNBase\Packet\FTS1::class, ]; protected array $header; // Packet Header protected ?string $name = NULL; // Packet name public File $file; // Packet filename protected Address $fftn_p; // Address the packet is from (when packing messages) protected Address $tftn_p; // Address the packet is to (when packing messages) protected Collection $messages; // Messages in the Packet protected string $content; // Outgoing packet data public Collection $errors; // Messages that fail validation protected int $index; // Our array index protected $pass_p = NULL; // Overwrite the packet password (when packing messages) /* ABSTRACT */ /** * This function is intended to be implemented in child classes to test if the packet * is defined by the child object * * @see self::PACKET_TYPES * @param string $header * @return bool */ abstract public static function is_type(string $header): bool; abstract protected function header(Collection $msgs): string; /* STATIC */ /** * Size of the packet header * * @return int */ public static function header_len(): int { return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); }); } /** * Process a packet file * * @param mixed $f File handler returning packet data * @param string $name * @param int $size * @param System|null $so - The system that sent us the packet, used to figure out domains if the packet is for a different zone * @param bool $process * @return Packet * @throws InvalidPacketException */ public static function process(mixed $f,string $name,int $size,System $so=NULL,bool $process=TRUE): self { Log::debug(sprintf('%s:+ Opening Packet [%s] with size [%d]',self::LOGKEY,$name,$size)); $o = FALSE; $header = ''; $read_ptr = 0; // Determine the type of packet foreach (self::PACKET_TYPES as $type) { $header_len = $type::header_len(); // PKT Header if ($read_ptr < $header_len) { $header .= fread($f,$header_len-$read_ptr); $read_ptr = ftell($f); } // Could not read header if (strlen($header) !== $header_len) throw new InvalidPacketException(sprintf('Length of header [%d] too short',strlen($header))); if ($type::is_type($header)) { $o = new $type($header); break; } } if (! $o) throw new InvalidPacketException('Cannot determine type of packet.'); $o->name = $name; $x = fread($f,2); if (strlen($x) === 2) { // End of Packet? if ($x === "\00\00") return $o; // Messages start with self::PACKED_MSG_LEAD elseif ($x !== self::PACKED_MSG_LEAD) throw new InvalidPacketException('Not a valid packet: '.bin2hex($x)); // No message attached } else throw new InvalidPacketException('Not a valid packet, not EOP or SOM:'.bin2hex($x)); Log::info(sprintf('%s:- Packet [%s] is a [%s] packet',self::LOGKEY,$o->name,get_class($o))); if ($o->fz && ($o->fd || $so)) { Log::alert(sprintf('%s:! No domain in the packet, work it out from the system [%d] for zone [%d]',self::LOGKEY,$so->name,$o->fz)); if (($x=$so->zones->where('zone_id',$o->fz)->unique('domain_id'))->count() === 1) { $o->zone = $x->pop(); } else { Log::alert(sprintf('%s:! Node [%s] has two zones with [%d]',self::LOGKEY,$so->name,$o->fz)); } } // If zone is not set, then we need to use a default zone - the messages may not be from this zone. if (empty($o->zone)) { Log::alert(sprintf('%s:! We couldnt work out the packet zone, so we have fallen back to the default for [%d]',self::LOGKEY,$o->fz)); try { $o->zone = Zone::where('zone_id',$o->fz) ->where('default',TRUE) ->singleOrFail(); } catch (ModelNotFoundException $e) { throw new InvalidPacketException(sprintf('%s:! We couldnt work out the packet zone, and there isnt a default for[%d]',self::LOGKEY,$o->fz)); } } Log::info(sprintf('%s:- Packet Dated [%s] from [%s] to [%s]',self::LOGKEY,$o->date,$o->fftn_t,$o->tftn_t)); $message = ''; // Current message we are building $msgbuf = ''; $leader = Message::header_len()+strlen(self::PACKED_MSG_LEAD); // We loop through reading from the buffer, to find our end of message tag while ((! feof($f) && ($readbuf=fread($f,$leader)))) { $read_ptr = ftell($f); $msgbuf .= $readbuf; // See if we have our EOM/EOP marker if ((($end=strpos($msgbuf,"\x00".self::PACKED_MSG_LEAD,$leader)) !== FALSE) || (($end=strpos($msgbuf,"\x00".self::PACKED_END,$leader)) !== FALSE)) { // Parse our message Log::debug(sprintf('%s:- Message at offset [%d] in [%s]',self::LOGKEY,$read_ptr-strlen($readbuf),$name)); $o->parseMessage(substr($msgbuf,0,$end),$process); $msgbuf = substr($msgbuf,$end+3); continue; // If we have more to read } elseif ($read_ptr < $size) { continue; } // If we get here throw new InvalidPacketException(sprintf('Cannot determine END of message/packet: %s|%s',get_class($o),hex_dump($message))); } if ($msgbuf) throw new InvalidPacketException(sprintf('Unprocessed data in packet: %s|%s',get_class($o),hex_dump($msgbuf))); return $o; } /** * @param string|null $header * @throws \Exception */ public function __construct(string $header=NULL) { $this->messages = collect(); $this->errors = collect(); if ($header) $this->header = unpack(self::unpackheader(static::HEADER),$header); } /** * @throws \Exception */ public function __get($key) { //Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key)); switch ($key) { // From Addresses case 'fz': return Arr::get($this->header,'ozone'); case 'fn': return Arr::get($this->header,'onet'); case 'ff': return Arr::get($this->header,'onode'); case 'fp': return Arr::get($this->header,'opoint'); case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00")); // To Addresses case 'tz': return Arr::get($this->header,'dzone'); case 'tn': return Arr::get($this->header,'dnet'); case 'tf': return Arr::get($this->header,'dnode'); case 'tp': return Arr::get($this->header,'dpoint'); case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00")); case 'date': return Carbon::create( Arr::get($this->header,'y'), Arr::get($this->header,'m')+1, Arr::get($this->header,'d'), Arr::get($this->header,'H'), Arr::get($this->header,'M'), Arr::get($this->header,'S') ); case 'password': return rtrim(Arr::get($this->header,$key),"\x00"); case 'fftn_t': case 'fftn': case 'tftn_t': case 'tftn': return parent::__get($key); case 'product': return Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo'); case 'software': Software::unguard(); $o = Software::singleOrNew(['code'=>$this->product,'type'=>Software::SOFTWARE_TOSSER]); Software::reguard(); return $o; case 'software_ver': return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min')); case 'capability': // This needs to be defined in child classes, since not all children have it return NULL; // Packet Type case 'type': return static::TYPE; // Packet name: case 'name': return $this->{$key} ?: sprintf('%08x',timew()); case 'messages': return $this->{$key}; default: throw new \Exception('Unknown key: '.$key); } } /** * Return the packet * * @return string * @throws \Exception */ public function __toString(): string { return $this->content; } /* INTERFACE */ /** * Number of messages in this packet */ public function count(): int { return $this->messages->count(); } public function current(): Echomail|Netmail { return $this->messages->get($this->index); } public function key(): mixed { return $this->index; } public function next(): void { $this->index++; } public function rewind(): void { $this->index = 0; } public function valid(): bool { return (! is_null($this->key())) && $this->messages->has($this->key()); } /* METHODS */ public function for(Address $ao): self { $this->tftn_p = $ao; $this->fftn_p = our_address($ao); return $this; } /** * Generate a packet * * @return string */ public function generate(): string { return (string)$this; } public function mail(Collection $msgs): self { if (! $msgs->count()) throw new InvalidPacketException('Refusing to make an empty packet'); if (empty($this->tftn_p) || empty($this->fftn_p)) throw new InvalidPacketException('Cannot generate a packet without a destination address'); $this->content = $this->header($msgs); foreach ($msgs as $o) $this->content .= self::PACKED_MSG_LEAD.$o->packet($this->tftn_p); $this->content .= "\00\00"; $this->messages = $msgs->map(fn($item)=>$item->only(['id','datetime'])); return $this; } /** * Parse a message in a mail packet * * @param string $message * @param bool $process * @throws \Exception */ private function parseMessage(string $message,bool $process): void { Log::info(sprintf('%s:+ Processing packet message [%d] bytes',self::LOGKEY,strlen($message))); $msg = Message::parseMessage($message,$this->zone); // If the message is invalid, we'll ignore it if ($process && $msg->errors->count()) { Log::info(sprintf('%s:- Message [%s] has [%d] errors',self::LOGKEY,$msg->msgid ?: 'No ID',$msg->errors->count())); // If the messages is not for the right zone, we'll ignore it if ($msg->errors->has('invalid-zone')) { Log::alert(sprintf('%s:! Message [%s] is from|to an invalid zone [%s|%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->msgid,$msg->get_fftn,$msg->get_tftn,$this->fz)); if (! $msg->kludges->get('RESCANNED')) Notification::route('netmail',$this->fftn)->notify(($msg instanceof Echomail) ? new EchomailBadAddress($msg) : new NetmailBadAddress($msg)); return; } // If the $msg->fftn doesnt exist, we'll need to create it if ($msg->errors->has('from') && $this->fftn && $this->fftn->zone_id) { Log::debug(sprintf('%s:^ From address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_fftn'))); $ao = Address::findFTN($msg->set->get('set_fftn'),TRUE,TRUE); if ($ao?->exists && ($ao->zone?->domain_id !== $this->fftn->zone->domain_id)) { Log::alert(sprintf('%s:! From address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_fftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id)); return; } if (! $ao) { $so = System::createUnknownSystem(); $ao = Address::createFTN($msg->set->get('set_fftn'),$so); Log::alert(sprintf('%s:- From FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_fftn'),$ao->id)); } $msg->fftn_id = $ao->id; $msg->errors->forget('from'); $msg->errors->forget('fftn_id'); } // If the $msg->tftn doesnt exist, we'll need to create it if ($msg->errors->has('to') && $this->tftn && $this->tftn->zone_id) { Log::debug(sprintf('%s:^ To address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_tftn'))); $ao = Address::findFTN($msg->set->get('set_tftn'),TRUE,TRUE); if ($ao?->exists && ($ao->zone?->domain_id !== $this->tftn->zone->domain_id)) { Log::alert(sprintf('%s:! To address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_tftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id)); return; } if (! $ao) { $so = System::createUnknownSystem(); $ao = Address::createFTN($msg->set->get('set_fftn'),$so); Log::alert(sprintf('%s:- To FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_tftn'),$ao->id)); } $msg->tftn_id = $ao->id; $msg->errors->forget('to'); $msg->errors->forget('tftn_id'); } // If there is no fftn, then its from a system that we dont know about if (! $this->fftn) { Log::alert(sprintf('%s:! No further message processing, packet is from a system we dont know about [%s]',self::LOGKEY,$this->fftn_t)); $this->messages->push($msg); return; } } // @todo If the message from domain (eg: $msg->fftn->zone->domain) is different to the packet address domain ($pkt->fftn->zone->domain), we'll skip this message Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id)); $this->messages->push($msg); } /** * Overwrite the packet password * * @param string|null $password * @return self */ public function password(string $password=NULL): self { if ($password && (strlen($password) < 9)) $this->pass_p = strtoupper($password); return $this; } }