diff --git a/app/Classes/FTN.php b/app/Classes/FTN.php index 568a8c9..10fe3b9 100644 --- a/app/Classes/FTN.php +++ b/app/Classes/FTN.php @@ -59,12 +59,22 @@ abstract class FTN */ protected static function unpackheader(array $pack): string { - return join('/', - collect($pack) - ->sortBy(function($k,$v) {return $k[0];}) - ->transform(function($k,$v) {return $k[1].$v;}) - ->values() - ->toArray() - ); + // Check that our header is correct + if (app()->environment('local')) { + $c = 0; + foreach (collect($pack) as $item) { + if ($c !== $item[0]) + throw new \Exception('Invalid header'); + + $c += $item[2]; + } + } + + return collect($pack) + ->sortBy(function($k,$v) {return $k[0];}) + ->transform(function($k,$v) {return $k[1].$v;}) + ->values() + ->join('/'); + ; } } \ No newline at end of file diff --git a/app/Classes/FTN/Message.php b/app/Classes/FTN/Message.php index b946764..766c6f0 100644 --- a/app/Classes/FTN/Message.php +++ b/app/Classes/FTN/Message.php @@ -71,7 +71,7 @@ class Message extends FTNBase public const FLAG_ECHOMAIL = 1<<16; // FTS-0001.016 Message header 32 bytes node, net, flags, cost, date - private const HEADER_LEN = 0x20; // Length of message header + public const HEADER_LEN = 0x20; // Length of message header private const header = [ // Struct of message header 'onode' => [0x00,'v',2], // Originating Node 'dnode' => [0x02,'v',2], // Destination Node diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index 90ce66a..3eb0e9c 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -10,7 +10,7 @@ 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}; +use App\Models\{Address,Software,System,Zone}; /** * Represents the structure of a packet @@ -19,102 +19,152 @@ 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; + protected const PACKED_MSG_LEAD = "\02\00"; - // 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 const PACKET_TYPES = [ + FTNBase\Packet\FSC45::class, + FTNBase\Packet\FSC48::class, + FTNBase\Packet\FSC39::class, + FTNBase\Packet\FTS1::class, ]; - private array $header; // Packet Header + 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 - private string $name; // Packet name public bool $use_cache = FALSE; // Use a cache for messages. - private int $index; // Our array index + protected int $index; // Our array index /** - * Number of messages in this packet + * @param string|null $header + * @throws \Exception */ - 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) + public function __construct(string $header=NULL) { $this->messages = collect(); $this->errors = collect(); $this->domain = NULL; + $this->name = 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); + 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 */ + + /** + * Site 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 * @@ -126,40 +176,48 @@ class Packet extends FTNBase implements \Iterator, \Countable * @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)); + $o = FALSE; + $header = ''; $read_ptr = 0; - // PKT Header - $header = fread($f,self::HEADER_LEN); - $read_ptr += strlen($header); + // Determine the type of packet + foreach (self::PACKET_TYPES as $type) { + $header_len = $type::header_len(); - // Could not read header - if (strlen($header) != self::HEADER_LEN) - throw new InvalidPacketException(sprintf('Length of header [%d] too short'.strlen($header))); + // PKT Header + if ($read_ptr < $header_len) { + $header .= fread($f,$header_len-$read_ptr); + $read_ptr = ftell($f); + } - // 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); + // 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 = 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; + if ((strlen($x) === 2) && ($x === "\00\00")) + return $o; - // Messages start with 02H 00H - if (strlen($x) == 2 AND $x != "\02\00") + // Messages start with self::PACKED_MSG_LEAD + if ((strlen($x) === 2) && ($x !== self::PACKED_MSG_LEAD)) throw new InvalidPacketException('Not a valid packet: '.bin2hex($x)); // No message attached @@ -179,10 +237,11 @@ class Packet extends FTNBase implements \Iterator, \Countable while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) { if (! $buf_ptr) - $read_ptr += strlen($readbuf); // Could use ftell() + $read_ptr = ftell($f); - if (strlen($message) < self::PACKED_MSG_HEADER_LEN) { - $addchars = self::PACKED_MSG_HEADER_LEN-strlen($message); + // Packed messages are Message::HEADER_LEN, prefixed with self::PACKED_MSG_LEAD + if (strlen($message) < (Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD))) { + $addchars = (Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD))-strlen($message); $message .= substr($readbuf,$buf_ptr,$addchars); $buf_ptr += $addchars; @@ -197,7 +256,7 @@ class Packet extends FTNBase implements \Iterator, \Countable if ($last && ($buf_ptr == 0)) { $last .= substr($readbuf,0,2); - if (($end=strpos($last,"\x00\x02\x00",$buf_ptr)) !== FALSE) { + if (($end=strpos($last,"\x00".self::PACKED_MSG_LEAD,$buf_ptr)) !== FALSE) { $o->parseMessage(substr($message,0,$end-2)); $last = ''; $message = ''; @@ -210,7 +269,7 @@ class Packet extends FTNBase implements \Iterator, \Countable $last = ''; } - if (($end=strpos($readbuf,"\x00\x02\x00",$buf_ptr)) === FALSE) { + if (($end=strpos($readbuf,"\x00".self::PACKED_MSG_LEAD,$buf_ptr)) === FALSE) { // Just in case our packet break is at the end of the buffer $last = substr($readbuf,-2); @@ -253,137 +312,89 @@ class Packet extends FTNBase implements \Iterator, \Countable } /** - * @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 + * Location of the version * - * @return string - * @throws \Exception + * @return int */ - public function __toString(): string + public static function version_offset(): int { - $return = $this->createHeader(); - - foreach ($this->messages as $o) { - if ($o->packed) - $return .= "\02\00".(string)$o; - } - - $return .= "\00\00"; - - return $return; + return Arr::get(collect(static::HEADER)->get('type'),0); } - /** - * Create our message packet header - */ - private function createHeader(): string + public static function version_offset_len(): int { - 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 Arr::get(collect(static::HEADER)->get('type'),2); + } - return $a; + /* INTERFACE */ - } catch (\Exception $e) { - return $e->getMessage(); - } + /** + * 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())); + } + + /* METHODS */ + + /** + * When creating a new packet, set the header. + * + * @param Address $oo + * @param Address $o + */ + public function addressHeader(Address $oo,Address $o): void + { + Log::debug(sprintf('%s:Creating packet [%s]',self::LOGKEY,$this->name)); + + $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' => $o->session('pktpass'), // Packet Password + ]; } /** @@ -396,38 +407,6 @@ class Packet extends FTNBase implements \Iterator, \Countable $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 * diff --git a/app/Classes/FTN/Packet/FSC39.php b/app/Classes/FTN/Packet/FSC39.php new file mode 100644 index 0000000..3aea8c7 --- /dev/null +++ b/app/Classes/FTN/Packet/FSC39.php @@ -0,0 +1,121 @@ + [0x00,'v',2], // Orig Node + 'dnode' => [0x02,'v',2], // Dest 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 + 'type' => [0x12,'v',2], // Packet Version (should be 2) + 'onet' => [0x14,'v',2], // Orig Net + 'dnet' => [0x16,'v',2], // Dest Net + 'prodcode-lo' => [0x18,'C',1], // Product Code + 'prodrev-maj' => [0x19,'C',1], // Product Version Major + 'password' => [0x1a,'a8',8], // Packet Password + 'ozone' => [0x22,'v',2], // Orig Zone + 'dzone' => [0x24,'v',2], // Dest Zone + 'reserved' => [0x26,'a2',2], // Reserved + 'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c) + 'prodcode-hi' => [0x2a,'C',1], // Product Code Hi + 'prodrev-min' => [0x2b,'C',1], // Product Version Minor + 'capword' => [0x2c,'v',2], // Capability Word + 'dozone' => [0x2e,'v',2], // Orig Zone + 'ddzone' => [0x30,'v',2], // Dest Zone + 'opoint' => [0x32,'v',2], // Orig Point + 'dpoint' => [0x34,'v',2], // Dest Point + 'proddata' => [0x36,'a4',4], // ProdData + ]; + + public const TYPE = '2e'; + + public function __get($key) + { + switch ($key) { + case 'capability': + return sprintf('%016b',Arr::get($this->header,'capword')); + + default: + return parent::__get($key); + } + } + + /** + * Create our message packet header + */ + protected function header(): string + { + try { + return pack(collect(self::HEADER)->pluck(1)->join(''), + $this->ff, // Orig Node + $this->tf, // Dest Node + Arr::get($this->header,'y'), // Year + Arr::get($this->header,'m'), // Month + Arr::get($this->header,'d'), // Day + Arr::get($this->header,'H'), // Hour + Arr::get($this->header,'M'), // Minute + Arr::get($this->header,'S'), // Second + 0, // Baud + 2, // Packet Version (should be 2) + $this->fn, // Orig Net + $this->tn, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code Lo + Setup::PRODUCT_VERSION_MAJ, // Product Version Major + $this->password, // Packet Password + $this->fz, // Orig Zone + $this->tz, // Dest Zone + '', // Reserved + Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c) + ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi + Setup::PRODUCT_VERSION_MIN, // Product Version Minor + Arr::get($this->header,'capword',1<<0), // Capability Word + $this->fz, // Orig Zone + $this->tz, // Dest Zone + $this->fp, // Orig Point + $this->tp, // Dest Point + strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData + ); + + } catch (\Exception $e) { + return $e->getMessage(); + } + } + + /** + * Determine if this is a fsc-0045 packet + * + * @param string $header + * @return bool + * @throws \Exception + */ + public static function is_type(string $header): bool + { + $head = unpack(self::unpackheader(self::HEADER),$header); + + return ( + (Arr::get($head,'type') === 2) + && (strlen(rtrim(Arr::get($head,'reserved'),"\x00")) === 0) + && (Arr::get($head,'capword') === Arr::get($head,'capvalid')) + && (Arr::get($head,'ozone') === Arr::get($head,'dozone')) + && (Arr::get($head,'dzone') === Arr::get($head,'ddzone')) + ); + } +} \ No newline at end of file diff --git a/app/Classes/FTN/Packet/FSC45.php b/app/Classes/FTN/Packet/FSC45.php new file mode 100644 index 0000000..b22c35a --- /dev/null +++ b/app/Classes/FTN/Packet/FSC45.php @@ -0,0 +1,91 @@ + [0x00,'v',2], // Orig Node + 'dnode' => [0x02,'v',2], // Dest Node + 'opoint' => [0x04,'v',2], // Orig Point + 'dpoint' => [0x06,'v',2], // Dest Point + 'reserved' => [0x08,'a8',8], // Reserved + 'subver' => [0x10,'v',2], // Sub Version (should be 2) + 'type' => [0x12,'v',2], // Packet Version (should be 2) + 'onet' => [0x14,'v',2], // Orig Net + 'dnet' => [0x16,'v',2], // Dest Net + 'prodcode' => [0x18,'C',1], // Product Code + 'prodrev-maj' => [0x19,'C',1], // Product Version + 'password' => [0x1a,'a8',8], // Packet Password + 'ozone' => [0x22,'v',2], // Orig Zone + 'dzone' => [0x24,'v',2], // Dest Zone + 'odomain' => [0x26,'a8',8], // Orig Domain + 'ddomain' => [0x2e,'a8',8], // Dest Domain + 'proddata' => [0x36,'a4',4], // ProdData + ]; + + public const TYPE = '2.2'; + + /** + * Create our message packet header + */ + protected function header(): string + { + try { + return pack(collect(self::HEADER)->pluck(1)->join(''), + $this->ff, // Orig Node + $this->tf, // Dest Node + $this->fp, // Orig Point + $this->tp, // Dest Point + '', // Reserved + 2, // Sub Version (should be 2) + 2, // Packet Version (should be 2) + $this->fn, // Orig Net + $this->tn, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code + Setup::PRODUCT_VERSION_MAJ, // Product Version + $this->password, // Packet Password + $this->fz, // Orig Zone + $this->tz, // Dest Zone + $this->fd, // Orig Domain + $this->td, // Dest Domain + strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData + ); + + } catch (\Exception $e) { + return $e->getMessage(); + } + } + + /** + * Determine if this is a fsc-0045 packet + * + * @param string $header + * @return bool + * @throws \Exception + */ + public static function is_type(string $header): bool + { + $head = unpack(self::unpackheader(self::HEADER),$header); + + return ( + (Arr::get($head,'type') === 2) + && ((Arr::get($head,'opoint') === 0) || (Arr::get($head,'onet') !== 0xffff)) + && (strlen(rtrim(Arr::get($head,'reserved'),"\x00")) === 0) + && (Arr::get($head,'subver') === 2) + && (Arr::get($head,'odomain')) + && (Arr::get($head,'ddomain')) + ); + } +} \ No newline at end of file diff --git a/app/Classes/FTN/Packet/FSC48.php b/app/Classes/FTN/Packet/FSC48.php new file mode 100644 index 0000000..f986184 --- /dev/null +++ b/app/Classes/FTN/Packet/FSC48.php @@ -0,0 +1,119 @@ + [0x00,'v',2], // Orig Node + 'dnode' => [0x02,'v',2], // Dest 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 + 'type' => [0x12,'v',2], // Packet Version (should be 2) + 'onet' => [0x14,'v',2], // Orig Net (0xFFFF when OrigPoint != 0) + 'dnet' => [0x16,'v',2], // Dest Net + 'prodcode-lo' => [0x18,'C',1], // Product Code Lo + 'prodrev-maj' => [0x19,'C',1], // Product Version Major + 'password' => [0x1a,'a8',8], // Packet Password + 'ozone' => [0x22,'v',2], // Orig Zone + 'dzone' => [0x24,'v',2], // Dest Zone + 'auxnet' => [0x26,'v',2], // Aux Net + 'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c) + 'prodcode-hi' => [0x2a,'C',1], // Product Code Hi + 'prodrev-min' => [0x2b,'C',1], // Product Version Minor + 'capword' => [0x2c,'v',2], // Capability Word + 'dozone' => [0x2e,'v',2], // Orig Zone + 'ddzone' => [0x30,'v',2], // Dest Zone + 'opoint' => [0x32,'v',2], // Orig Point + 'dpoint' => [0x34,'v',2], // Dest Point + 'proddata' => [0x36,'a4',4], // ProdData + ]; + + public const TYPE = '2+'; + + public function __get($key) + { + switch ($key) { + case 'capability': + return sprintf('%016b',Arr::get($this->header,'capword')); + + default: + return parent::__get($key); + } + } + + /** + * Create our message packet header + */ + protected function header(): string + { + try { + return pack(collect(self::HEADER)->pluck(1)->join(''), + $this->ff, // Orig Node + $this->tf, // Dest Node + Arr::get($this->header,'y'), // Year + Arr::get($this->header,'m'), // Month + Arr::get($this->header,'d'), // Day + Arr::get($this->header,'H'), // Hour + Arr::get($this->header,'M'), // Minute + Arr::get($this->header,'S'), // Second + 0, // Baud + 2, // Packet Version (should be 2) + $this->fp ? 0xffff : $this->fn, // Orig Net (0xFFFF when OrigPoint != 0) + $this->tn, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code Lo + Setup::PRODUCT_VERSION_MAJ, // Product Version Major + $this->password, // Packet Password + $this->fz, // Orig Zone + $this->tz, // Dest Zone + $this->fp ? $this->fn : 0x00, // Aux Net + Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c) + ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi + Setup::PRODUCT_VERSION_MIN, // Product Version Minor + Arr::get($this->header,'capword',1<<0), // Capability Word + $this->fz, // Orig Zone + $this->tz, // Dest Zone + $this->fp, // Orig Point + $this->tp, // Dest Point + strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData + ); + + } catch (\Exception $e) { + return $e->getMessage(); + } + } + + /** + * Determine if this is a fsc-0045 packet + * + * @param string $header + * @return bool + * @throws \Exception + */ + public static function is_type(string $header): bool + { + $head = unpack(self::unpackheader(self::HEADER),$header); + + return ( + (Arr::get($head,'type') === 2) + && (Arr::get($head,'capword') === Arr::get($head,'capvalid')) + && ((Arr::get($head,'opoint') === 0) || (Arr::get($head,'onet') === 0xffff)) + && Arr::get($head,'auxnet') + ); + } +} \ No newline at end of file diff --git a/app/Classes/FTN/Packet/FTS1.php b/app/Classes/FTN/Packet/FTS1.php new file mode 100644 index 0000000..ee67f1e --- /dev/null +++ b/app/Classes/FTN/Packet/FTS1.php @@ -0,0 +1,89 @@ + [0x00,'v',2], // Orig Node + 'dnode' => [0x02,'v',2], // Dest 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 + 'type' => [0x12,'v',2], // Packet Version (should be 2) + 'onet' => [0x14,'v',2], // Orig Net + 'dnet' => [0x16,'v',2], // Dest Net + 'prodcode' => [0x18,'C',1], // Product Code + 'serial' => [0x19,'C',1], // Serial Number + 'password' => [0x1a,'a8',8], // Packet Password + 'ozone' => [0x22,'v',2], // Orig Zone + 'dzone' => [0x24,'v',2], // Dest Zone + 'reserved' => [0x26,'a20',20], // Reserved + ]; + + public const TYPE = '2'; + + /** + * Create our message packet header + */ + protected function header(): string + { + try { + return pack(collect(self::HEADER)->pluck(1)->join(''), + $this->ff, // Orig Node + $this->tf, // Dest Node + Arr::get($this->header,'y'), // Year + Arr::get($this->header,'m'), // Month + Arr::get($this->header,'d'), // Day + Arr::get($this->header,'H'), // Hour + Arr::get($this->header,'M'), // Minute + Arr::get($this->header,'S'), // Second + 0, // Baud + 2, // Packet Version (should be 2) + $this->fn, // Orig Net + $this->tn, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code Lo + Setup::PRODUCT_VERSION_MAJ, // Product Version Major + $this->password, // Packet Password + $this->fz, // Orig Zone + $this->tz, // Dest Zone + '', // Reserved + ); + + } catch (\Exception $e) { + return $e->getMessage(); + } + } + + /** + * Determine if this is a fsc-0045 packet + * + * @param string $header + * @return bool + * @throws \Exception + */ + public static function is_type(string $header): bool + { + $head = unpack(self::unpackheader(self::HEADER),$header); + + return ( + (Arr::get($head,'type') === 2) + && (strlen(rtrim(Arr::get($head,'reserved'),"\x00")) === 0) + ); + } +} \ No newline at end of file diff --git a/app/Console/Commands/PacketInfo.php b/app/Console/Commands/PacketInfo.php index 5ce504e..0e46d48 100644 --- a/app/Console/Commands/PacketInfo.php +++ b/app/Console/Commands/PacketInfo.php @@ -42,7 +42,7 @@ class PacketInfo extends Command $this->alert(sprintf('File Name: %s',$x)); - $this->info(sprintf('Packet Type : %s',$pkt->type)); + $this->info(sprintf('Packet Type : %s (%s)',$pkt->type,get_class($pkt))); $this->info(sprintf('From : %s to %s',$pkt->fftn,$pkt->tftn)); $this->info(sprintf('Dated : %s',$pkt->date)); $this->info(sprintf('Password : %s (%s)',$pkt->password,$pkt->password ? 'SET' : 'NOT set')); diff --git a/app/Models/Address.php b/app/Models/Address.php index 98b06bb..85ba9ef 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -650,7 +650,9 @@ class Address extends Model if (! $ao) return NULL; - $o = new Packet($ao,$this); + // @todo make packet type configurable by each system + $o = new Packet\FSC48; + $o->addressHeader($ao,$this); // $oo = Netmail/Echomail Model $c = 0; diff --git a/resources/views/about.blade.php b/resources/views/about.blade.php index 90f2e20..ef3c9c7 100644 --- a/resources/views/about.blade.php +++ b/resources/views/about.blade.php @@ -53,7 +53,8 @@ If you have more than 1 BBS, then the Clearing Houz can receive all your mail fr
  • Nodelist Management
  • Self service FTN Network Applications being implemented
  • Dynamic mail bundling for upstream and downstream nodes (no more "inbounds" and "outbounds")
  • -
  • Automatic delisting of idle nodes
  • +
  • Support for Fidonet Packet formats FTS-0001,FSC-0039,FSC-0045,FSC-0048
  • +
  • Automatic delisting of idle nodes (to be implemented)
  • DNS server, to enable resolving of registered nodes using domain dns names pN.fN.nN.zN.domain.ftn, or pN.fN.nN.zN.[domain dns zone]