535 lines
15 KiB
PHP
535 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Classes\FTN;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Symfony\Component\HttpFoundation\File\File;
|
|
|
|
use App\Classes\FTN as FTNBase;
|
|
use App\Models\{Address,Setup,Software,System,Zone};
|
|
|
|
class Packet extends FTNBase implements \Iterator, \Countable
|
|
{
|
|
private const LOGKEY = 'PKT';
|
|
|
|
private const HEADER_LEN = 0x3a;
|
|
private const VERSION_OFFSET = 0x12;
|
|
private const BLOCKSIZE = 1024;
|
|
private const PACKED_MSG_HEADER_LEN = 0x22;
|
|
|
|
// V2 Packet Header (2/2e/2+)
|
|
private const v2header = [
|
|
'onode' => [0x00,'v',2], // Originating Node
|
|
'dnode' => [0x02,'v',2], // Destination Node
|
|
'y' => [0x04,'v',2], // Year
|
|
'm' => [0x06,'v',2], // Month
|
|
'd' => [0x08,'v',2], // Day
|
|
'H' => [0x0a,'v',2], // Hour
|
|
'M' => [0x0c,'v',2], // Minute
|
|
'S' => [0x0e,'v',2], // Second
|
|
'baud' => [0x10,'v',2], // Baud
|
|
'pktver' => [0x12,'v',2], // Packet Version
|
|
'onet' => [0x14,'v',2], // Originating Net (0xffff when origPoint !=0 2+)
|
|
'dnet' => [0x16,'v',2], // Destination Net
|
|
'prodcode-lo' => [0x18,'C',1],
|
|
'prodrev-maj' => [0x19,'C',1], // Product Version Major (serialNum 2)
|
|
'password' => [0x1a,'Z8',8], // Packet Password
|
|
'qozone' => [0x22,'v',2],
|
|
'qdzone' => [0x24,'v',2],
|
|
'filler' => [0x26,'v',2], // Reserved (auxnet 2+ - contains Orignet if Origin is a point) fsc-0048.001
|
|
'capvalid' => [0x28,'n',2], // fsc-0039.004 (Not used 2) (copy of 0x2c)
|
|
'prodcode-hi' => [0x2a,'C',1], // (Not used 2)
|
|
'prodrev-min' => [0x2b,'C',1], // (Not used 2)
|
|
'capword' => [0x2c,'v',2], // fsc-0039.001 (Not used 2)
|
|
'ozone' => [0x2e,'v',2], // Originating Zone (Not used 2)
|
|
'dzone' => [0x30,'v',2], // Destination Zone (Not used 2)
|
|
'opoint' => [0x32,'v',2], // Originating Point (Not used 2)
|
|
'dpoint' => [0x34,'v',2], // Destination Point (Not used 2)
|
|
'proddata' => [0x36,'a4',4], // ProdData (Not used 2) // FSC-39/FSC-48
|
|
];
|
|
|
|
private array $header; // Packet Header
|
|
|
|
public File $file; // Packet filename
|
|
public Collection $messages; // Messages in the Packet
|
|
public Collection $errors; // Messages that fail validation
|
|
private string $name; // Packet name
|
|
public bool $use_cache = FALSE; // Use a cache for messages.
|
|
private int $index; // Our array index
|
|
|
|
/**
|
|
* Number of messages in this packet
|
|
*/
|
|
public function count(): int
|
|
{
|
|
return $this->messages->count();
|
|
}
|
|
|
|
public function current(): Message
|
|
{
|
|
return $this->use_cache ? unserialize(Cache::pull($this->key())) : $this->messages->get($this->index);
|
|
}
|
|
|
|
public function key(): mixed
|
|
{
|
|
return $this->use_cache ? $this->messages->get($this->index) : $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->use_cache ? Cache::has($this->key()) : $this->messages->has($this->key()));
|
|
}
|
|
|
|
/**
|
|
* @param Address|NULL $oo Origin Address
|
|
* @param Address|NULL $o Destination Address
|
|
*/
|
|
public function __construct(Address $oo=NULL,Address $o=NULL)
|
|
{
|
|
$this->messages = collect();
|
|
$this->errors = collect();
|
|
$this->domain = NULL;
|
|
|
|
// If we are creating an outbound packet, we need to set our header
|
|
if ($oo && $o) {
|
|
$this->name = sprintf('%08x',timew());
|
|
Log::debug(sprintf('%s:Creating packet [%s]',self::LOGKEY,$this->name));
|
|
$this->newHeader($oo,$o);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a packet file
|
|
*
|
|
* @param mixed $f
|
|
* @param string $name
|
|
* @param int $size
|
|
* @param System|null $system
|
|
* @param bool $use_cache
|
|
* @return Packet
|
|
* @throws InvalidPacketException
|
|
*/
|
|
|
|
public static function process(mixed $f,string $name,int $size,System $system=NULL,bool $use_cache=FALSE): self
|
|
{
|
|
Log::debug(sprintf('%s:+ Opening Packet [%s] with size [%d]',self::LOGKEY,$name,$size));
|
|
|
|
$read_ptr = 0;
|
|
|
|
// PKT Header
|
|
$header = fread($f,self::HEADER_LEN);
|
|
$read_ptr += strlen($header);
|
|
|
|
// Could not read header
|
|
if (strlen($header) != self::HEADER_LEN)
|
|
throw new InvalidPacketException(sprintf('Length of header [%d] too short'.strlen($header)));
|
|
|
|
// Not a type 2 packet
|
|
$version = Arr::get(unpack('vv',substr($header,self::VERSION_OFFSET)),'v');
|
|
if ($version != 2)
|
|
throw new InvalidPacketException('Not a type 2 packet: '.$version);
|
|
|
|
$o = new self;
|
|
$o->use_cache = $use_cache;
|
|
$o->name = $name;
|
|
$o->header = unpack(self::unpackheader(self::v2header),$header);
|
|
|
|
$x = fread($f,2);
|
|
$read_ptr += strlen($x);
|
|
|
|
// End of Packet?
|
|
if (strlen($x) == 2 and $x == "\00\00")
|
|
return new self;
|
|
|
|
// Messages start with 02H 00H
|
|
if (strlen($x) == 2 AND $x != "\02\00")
|
|
throw new InvalidPacketException('Not a valid packet: '.bin2hex($x));
|
|
|
|
// No message attached
|
|
else if (! strlen($x))
|
|
throw new InvalidPacketException('No message in packet: '.bin2hex($x));
|
|
|
|
$o->zone = $system?->zones->firstWhere('zone_id',$o->fz);
|
|
|
|
// If zone is null, we'll take the zone from the packet
|
|
if (! $o->zone)
|
|
$o->zone = Zone::where('zone_id',$o->fz)->where('default',TRUE)->single();
|
|
|
|
$buf_ptr = 0;
|
|
$message = '';
|
|
$readbuf = '';
|
|
$last = '';
|
|
|
|
while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) {
|
|
if (! $buf_ptr)
|
|
$read_ptr += strlen($readbuf); // Could use ftell()
|
|
|
|
if (strlen($message) < self::PACKED_MSG_HEADER_LEN) {
|
|
$addchars = self::PACKED_MSG_HEADER_LEN-strlen($message);
|
|
$message .= substr($readbuf,$buf_ptr,$addchars);
|
|
$buf_ptr += $addchars;
|
|
|
|
// If our buffer wasnt big enough...
|
|
if ($buf_ptr >= strlen($readbuf)) {
|
|
$buf_ptr = 0;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Take 2 chars from the buffer and check if we have our end packet signature
|
|
if ($last && ($buf_ptr == 0)) {
|
|
$last .= substr($readbuf,0,2);
|
|
|
|
if (($end=strpos($last,"\x00\x02\x00",$buf_ptr)) !== FALSE) {
|
|
$o->parseMessage(substr($message,0,$end-2));
|
|
$last = '';
|
|
$message = '';
|
|
$buf_ptr = 1+$end;
|
|
|
|
// Loop to rebuild our header for the next message
|
|
continue;
|
|
}
|
|
|
|
$last = '';
|
|
}
|
|
|
|
if (($end=strpos($readbuf,"\x00\x02\x00",$buf_ptr)) === FALSE) {
|
|
// Just in case our packet break is at the end of the buffer
|
|
$last = substr($readbuf,-2);
|
|
|
|
if ((str_contains($last,"\x00")) && ($size-$read_ptr > 2)) {
|
|
$message .= substr($readbuf,$buf_ptr);
|
|
$buf_ptr = 0;
|
|
|
|
continue;
|
|
}
|
|
|
|
$last = '';
|
|
$end = strpos($readbuf,"\x00\x00\x00",$buf_ptr);
|
|
}
|
|
|
|
// See if we have found the end of the packet, if not read more.
|
|
if ($end === FALSE && ($read_ptr < $size)) {
|
|
$message .= substr($readbuf,$buf_ptr);
|
|
$buf_ptr = 0;
|
|
|
|
continue;
|
|
|
|
} else {
|
|
$message .= substr($readbuf,$buf_ptr,$end-$buf_ptr);
|
|
$buf_ptr = $end+3;
|
|
|
|
if ($buf_ptr >= strlen($readbuf))
|
|
$buf_ptr = 0;
|
|
}
|
|
|
|
// Look for the next message
|
|
$o->parseMessage($message);
|
|
$message = '';
|
|
}
|
|
|
|
// If our message is still set, then we have an unprocessed message
|
|
if ($message)
|
|
$o->parseMessage($message);
|
|
|
|
return $o;
|
|
}
|
|
|
|
/**
|
|
* @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 Arr::get($this->header,'odomain');
|
|
|
|
// 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 Arr::get($this->header,'ddomain');
|
|
|
|
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 'capability':
|
|
return Arr::get($this->header,'capword') == Arr::get($this->header,'capvalid') ? sprintf('%016b',Arr::get($this->header,'capword')) : 'FTS-1';
|
|
|
|
case 'password':
|
|
return Arr::get($this->header,$key);
|
|
|
|
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'));
|
|
|
|
// Packet Type
|
|
case 'type':
|
|
if ((Arr::get($this->header,'onet') == 0xffff) && (Arr::get($this->header,'opoint') != 0) && Arr::get($this->header,'filler'))
|
|
return '2+';
|
|
elseif (Arr::get($this->header,'prodrev-maj') && ! Arr::get($this->header,'capword'))
|
|
return '2';
|
|
else
|
|
return '2e';
|
|
|
|
// Packet name:
|
|
case 'name':
|
|
return $this->{$key};
|
|
|
|
default:
|
|
throw new \Exception('Unknown key: '.$key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the packet
|
|
*
|
|
* @return string
|
|
* @throws \Exception
|
|
*/
|
|
public function __toString(): string
|
|
{
|
|
$return = $this->createHeader();
|
|
|
|
foreach ($this->messages as $o) {
|
|
if ($o->packed)
|
|
$return .= "\02\00".(string)$o;
|
|
}
|
|
|
|
$return .= "\00\00";
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Create our message packet header
|
|
*/
|
|
private function createHeader(): string
|
|
{
|
|
try {
|
|
$a = pack(collect(self::v2header)->merge(['password' => [0x1a,'a8',8]])->pluck(1)->join(''),
|
|
$this->ff,
|
|
$this->tf,
|
|
Arr::get($this->header,'y'),
|
|
Arr::get($this->header,'m'),
|
|
Arr::get($this->header,'d'),
|
|
Arr::get($this->header,'H'),
|
|
Arr::get($this->header,'M'),
|
|
Arr::get($this->header,'S'),
|
|
Arr::get($this->header,'baud',0),
|
|
Arr::get($this->header,'pktver',2),
|
|
$this->fn, // @todo if point, this needs to be 0xff
|
|
$this->tn,
|
|
Arr::get($this->header,'prodcode-lo',(Setup::PRODUCT_ID & 0xff)),
|
|
Arr::get($this->header,'prodrev-maj',Setup::PRODUCT_VERSION_MAJ),
|
|
$this->password,
|
|
$this->fz,
|
|
$this->tz,
|
|
Arr::get($this->header,'filler',''),
|
|
Arr::get($this->header,'capvalid',1<<0),
|
|
Arr::get($this->header,'prodcode-hi',(Setup::PRODUCT_ID >> 8) & 0xff),
|
|
Arr::get($this->header,'prodrev-min',Setup::PRODUCT_VERSION_MIN),
|
|
Arr::get($this->header,'capword',1<<0),
|
|
$this->fz,
|
|
$this->tz,
|
|
$this->fp,
|
|
$this->tp,
|
|
Arr::get($this->header,'proddata','AB8D'),
|
|
);
|
|
|
|
return $a;
|
|
|
|
} catch (\Exception $e) {
|
|
return $e->getMessage();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a netmail message to this packet
|
|
*
|
|
* @param Message $o
|
|
*/
|
|
public function addMail(Message $o): void
|
|
{
|
|
$this->messages->push($o);
|
|
}
|
|
|
|
/**
|
|
* When creating a new packet, set the header.
|
|
*
|
|
* @param Address $oo
|
|
* @param Address $o
|
|
*/
|
|
private function newHeader(Address $oo,Address $o): void
|
|
{
|
|
$date = Carbon::now();
|
|
|
|
// Create Header
|
|
$this->header = [
|
|
'onode' => $oo->node_id, // Originating Node
|
|
'dnode' => $o->node_id, // Destination Node
|
|
'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
|
|
'onet' => $oo->host_id ?: $oo->region_id, // Originating Net (0xffff when origPoint !=0 2+)
|
|
'dnet' => $o->host_id ?: $o->region_id, // Destination Net
|
|
'password' => $o->session('pktpass'), // Packet Password
|
|
'qozone' => $oo->zone->zone_id,
|
|
'qdzone' => $o->zone->zone_id,
|
|
'ozone' => $oo->zone->zone_id, // Originating Zone (Not used 2)
|
|
'dzone' => $o->zone->zone_id, // Destination Zone (Not used 2)
|
|
'opoint' => $oo->point_id, // Originating Point (Not used 2)
|
|
'dpoint' => $o->point_id, // Destination Point (Not used 2)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse a message in a mail packet
|
|
*
|
|
* @param string $message
|
|
* @throws InvalidPacketException|\Exception
|
|
*/
|
|
private function parseMessage(string $message): void
|
|
{
|
|
Log::info(sprintf('%s:Processing message [%d] bytes',self::LOGKEY,strlen($message)));
|
|
|
|
$msg = Message::parseMessage($message,$this->zone);
|
|
|
|
// 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 from 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();
|
|
|
|
} 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;
|
|
|
|
System::unguard();
|
|
$so = System::firstOrCreate([
|
|
'name' => 'Discovered System',
|
|
'sysop' => 'Unknown',
|
|
'location' => '',
|
|
'active' => TRUE,
|
|
]);
|
|
System::reguard();
|
|
|
|
$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 ($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();
|
|
|
|
} 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;
|
|
|
|
System::unguard();
|
|
$so = System::firstOrCreate([
|
|
'name' => 'Discovered System',
|
|
'sysop' => 'Unknown',
|
|
'location' => '',
|
|
'active' => TRUE,
|
|
]);
|
|
System::reguard();
|
|
|
|
$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;
|
|
}
|
|
}
|
|
|
|
if ($this->use_cache) {
|
|
$key = urlencode($msg->msgid ?: sprintf('%s %s',$msg->fftn,Carbon::now()->timestamp));
|
|
if (! Cache::forever($key,serialize($msg)))
|
|
throw new \Exception(sprintf('Caching failed for key [%s]?',$key));
|
|
|
|
$this->messages->push($key);
|
|
|
|
} else {
|
|
$this->messages->push($msg);
|
|
}
|
|
}
|
|
} |