clrghouz/app/Classes/FTN/Packet.php

373 lines
10 KiB
PHP

<?php
namespace App\Classes\FTN;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\File;
use App\Classes\FTN as FTNBase;
use App\Models\{Address,Domain,Setup,Software};
class Packet extends FTNBase
{
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 function __construct(Address $o=NULL)
{
$this->messages = collect();
$this->errors = collect();
$this->domain = NULL;
$this->name = sprintf('%08x',timew());
// If we are creating an outbound packet, we need to set our header
if ($o)
$this->newHeader($o);
}
/**
* Open a packet file
*
* @param File $file
* @param Domain|null $domain
* @return Packet
* @throws InvalidPacketException
*/
public static function open(File $file,Domain $domain=NULL): self
{
Log::debug(sprintf('%s:Opening Packet [%s]',self::LOGKEY,$file));
$f = fopen($file,'r');
$fstat = fstat($f);
// PKT Header
$header = fread($f,self::HEADER_LEN);
// 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->name = (string)$file;
$o->header = unpack(self::unpackheader(self::v2header),$header);
$x = fread($f,2);
// 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));
$buf_ptr = 0;
$message = '';
$readbuf = '';
while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) {
// A message header is atleast 0x22 chars long
if (strlen($readbuf) < self::PACKED_MSG_HEADER_LEN) {
$message .= $readbuf;
$buf_ptr = 0;
continue;
} elseif (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;
}
}
// If we didnt find a packet end, perhaps there are no more
if (($end=strpos($readbuf,"\x00\x02\x00",$buf_ptr)) === FALSE) {
$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 && (ftell($f) < $fstat['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,$domain);
$message = '';
}
// If our message is still set, then we have an unprocessed message
if ($message)
$o->parseMessage($message,$domain);
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 'tftn':
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
{
// Cache the packet creation
static $return = NULL;
if (is_null($return)) {
$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)->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 $o
*/
private function newHeader(Address $o): void
{
$date = Carbon::now();
$ao = Setup::findOrFail(config('app.id'))->system->match($o->zone)->first();
// Create Header
$this->header = [
'onode' => $ao->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' => $ao->host_id ?: $ao->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' => $ao->zone->zone_id,
'qdzone' => $o->zone->zone_id,
'ozone' => $ao->zone->zone_id, // Originating Zone (Not used 2)
'dzone' => $o->zone->zone_id, // Destination Zone (Not used 2)
'opoint' => $ao->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
* @param Domain|null $domain
* @throws InvalidPacketException
*/
public function parseMessage(string $message,Domain $domain=NULL): void
{
$msg = Message::parseMessage($message,$domain);
// If the message is invalid, we'll ignore it
if ($msg->errors && $msg->errors->messages()->has('from')) {
$this->errors->push($msg);
Log::error(sprintf('%s:%s Skipping...',self::LOGKEY,join('|',$msg->errors->messages()->get('from'))));
} else {
$this->messages->push($msg);
}
}
}