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; // Packet name public File $file; // Packet filename public Collection $messages; // Messages in the Packet public Collection $errors; // Messages that fail validation protected int $index; // Our array index /** * @param string|null $header * @throws \Exception */ public function __construct(string $header=NULL) { $this->messages = collect(); $this->errors = collect(); $this->domain = NULL; $this->name = NULL; if ($header) $this->header = unpack(self::unpackheader(static::HEADER),$header); } /** * @throws \Exception */ public function __get($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': case 'fftn_o': case 'tftn': case 'tftn_o': return parent::__get($key); case 'software': $code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo'); Software::unguard(); $o = Software::singleOrNew(['code'=>$code,'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()); default: throw new \Exception('Unknown key: '.$key); } } /** * Return the packet * * @return string * @throws \Exception */ public function __toString(): string { $return = $this->header(); foreach ($this->messages as $o) { if ($o->packed) $return .= self::PACKED_MSG_LEAD.$o; } $return .= "\00\00"; return $return; } /* 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); }); } /** * 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 */ public static function is_type(string $header): bool { return FALSE; } /** * Process a packet file * * @param mixed $f File handler returning packet data * @param string $name * @param int $size * @param Domain|null $domain * @return Packet * @throws InvalidPacketException */ public static function process(mixed $f,string $name,int $size,Domain $domain=NULL): 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)); // Work out the packet zone if ($o->fz && ($o->fd || $domain)) { $o->zone = Zone::select('zones.*') ->join('domains',['domains.id'=>'zones.domain_id']) ->where('zone_id',$o->fz) ->where('name',$o->fd ?: $domain->name) ->single(); // We need not knowing the domain, we use the default zone } else { $o->zone = Zone::where('zone_id',$o->fz) ->where('default',TRUE) ->single(); } // If zone is not set, then we need to use a default zone - the messages may not be from this zone. if (! $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)); $o->zone = Zone::where('zone_id',$o->fz) ->where('default',TRUE) ->singleOrFail(); } $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 $o->parseMessage(substr($msgbuf,0,$end)); $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; } /** * Location of the version * * @return int */ public static function version_offset(): int { return Arr::get(collect(static::HEADER)->get('type'),0); } public static function version_offset_len(): int { return Arr::get(collect(static::HEADER)->get('type'),2); } /* INTERFACE */ /** * Number of messages in this packet */ public function count(): int { return $this->messages->count(); } public function current(): Message { 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 */ /** * When creating a new packet, set the header. * * @param Address $oo * @param Address $o * @param string|null $passwd Override the password used in the packet */ public function addressHeader(Address $oo,Address $o,string $passwd=NULL): void { Log::debug(sprintf('%s:+ Creating packet for [%s]',self::LOGKEY,$o->ftn)); $date = Carbon::now(); // Create Header $this->header = [ 'ozone' => $oo->zone->zone_id, // Orig Zone 'dzone' => $o->zone->zone_id, // Dest Zone 'onet' => $oo->host_id ?: $oo->region_id, // Orig Net 'dnet' => $o->host_id ?: $o->region_id, // Dest Net 'onode' => $oo->node_id, // Orig Node 'dnode' => $o->node_id, // Dest Node 'opoint' => $oo->point_id, // Orig Point 'dpoint' => $o->point_id, // Dest Point 'odomain' => $oo->zone->domain->name, // Orig Domain 'ddomain' => $o->zone->domain->name, // Dest Domain 'y' => $date->format('Y'), // Year 'm' => $date->format('m')-1, // Month 'd' => $date->format('d'), // Day 'H' => $date->format('H'), // Hour 'M' => $date->format('i'), // Minute 'S' => $date->format('s'), // Second 'password' => strtoupper((! is_null($passwd)) ? $passwd : $o->session('pktpass')), // Packet Password ]; } /** * Add a message to this packet * * @param Message $o */ public function addMail(Message $o): void { $this->messages->push($o); } /** * Parse a message in a mail packet * * @param string $message * @throws InvalidPacketException|\Exception */ private function parseMessage(string $message): void { Log::info(sprintf('%s:+ Processing packet message [%d] bytes',self::LOGKEY,strlen($message))); $msg = Message::parseMessage($message,$this->zone); // If the message from domain is different to the packet address domain, we'll skip this message // If the message is invalid, we'll ignore it if ($msg->errors) { Log::info(sprintf('%s:- Message [%s] has errors',self::LOGKEY,$msg->msgid)); // If the messages is not for the right zone, we'll ignore it if ($msg->errors->messages()->has('invalid-zone')) { Log::alert(sprintf('%s:! Message is from an invalid zone [%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->fftn,$msg->zone->domain->name)); if (! $msg->rescanned->count()) Notification::route('netmail',$this->fftn_o)->notify(new EchomailBadAddress($msg)); return; } // If the to address doenst exist, we'll create a new entry if ($msg->errors->messages()->has('to') && $msg->tzone) { try { // @todo Need to work out the correct region for the host_id Address::unguard(); $ao = Address::firstOrNew([ 'zone_id' => $msg->tzone->id, //'region_id' => 0, 'host_id' => $msg->tn, 'node_id' => $msg->tf, 'point_id' => $msg->tp, 'active' => TRUE, ]); Address::reguard(); if (is_null($ao->region_id)) $ao->region_id = $ao->host_id; } catch (\Exception $e) { Log::error(sprintf('%s:! Error finding/creating TO address [%s] for message',self::LOGKEY,$msg->tboss),['error'=>$e->getMessage()]); $this->errors->push($msg); return; } $ao->role = Address::NODE_UNKNOWN; $so = System::createUnknownSystem(); $so->addresses()->save($ao); Log::alert(sprintf('%s:- To FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->tboss,$ao->id)); } // If the from address doenst exist, we'll create a new entry if ($msg->errors->messages()->has('from') && $msg->tzone) { try { // @todo Need to work out the correct region for the host_id Address::unguard(); $ao = Address::firstOrNew([ 'zone_id' => $msg->fzone->id, //'region_id' => 0, 'host_id' => $msg->fn, 'node_id' => $msg->ff, 'point_id' => $msg->fp, 'active'=> TRUE, ]); Address::reguard(); if (is_null($ao->region_id)) $ao->region_id = $ao->host_id; } catch (\Exception $e) { Log::error(sprintf('%s:! Error finding/creating FROM address [%s] for message',self::LOGKEY,$msg->fboss),['error'=>$e->getMessage()]); $this->errors->push($msg); return; } $ao->role = Address::NODE_UNKNOWN; $so = System::createUnknownSystem(); $so->addresses()->save($ao); Log::alert(sprintf('%s:- From FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->fboss,$ao->id)); } if ($msg->errors->messages()->has('user_from') || $msg->errors->messages()->has('user_to')) { Log::error(sprintf('%s:! Skipping message [%s] due to errors (%s)...',self::LOGKEY,$msg->msgid,join(',',$msg->errors->messages()->keys()))); $this->errors->push($msg); return; } } $this->messages->push($msg); } public function pluck(string $key): Collection { return $this->messages->pluck($key); } }