2021-06-29 10:43:29 +00:00
< ? php
namespace App\Classes\FTN ;
use Carbon\Carbon ;
2024-06-07 00:51:28 +00:00
use Illuminate\Database\Eloquent\Builder ;
2021-06-29 10:43:29 +00:00
use Illuminate\Support\Arr ;
use Illuminate\Support\Collection ;
use Illuminate\Support\Facades\Log ;
2023-09-20 10:29:23 +00:00
use Illuminate\Support\Facades\Notification ;
2021-06-29 10:43:29 +00:00
use Symfony\Component\HttpFoundation\File\File ;
use App\Classes\FTN as FTNBase ;
2024-05-17 12:10:54 +00:00
use App\Exceptions\InvalidPacketException ;
2024-05-20 11:31:21 +00:00
use App\Models\ { Address , Domain , Echomail , Netmail , Software , System , Zone };
2023-09-20 10:29:23 +00:00
use App\Notifications\Netmails\EchomailBadAddress ;
2021-06-29 10:43:29 +00:00
2023-06-22 07:36:22 +00:00
/**
2023-09-14 22:09:42 +00:00
* Represents a Fidonet Packet , that contains an array of messages .
*
2024-05-17 12:10:54 +00:00
* Thus this object is iterable as an array of Echomail :: class or Netmail :: class .
2023-06-22 07:36:22 +00:00
*/
2024-05-17 12:10:54 +00:00
abstract class Packet extends FTNBase implements \Iterator , \Countable
2021-06-29 10:43:29 +00:00
{
private const LOGKEY = 'PKT' ;
2023-06-25 10:45:02 +00:00
protected const PACKED_MSG_LEAD = " \02 \00 " ;
2023-12-13 12:00:47 +00:00
protected const PACKED_END = " \00 \00 " ;
2023-06-25 10:45:02 +00:00
2024-05-28 02:23:59 +00:00
public const MSG_TYPE2 = 1 << 0 ;
public const MSG_TYPE4 = 1 << 2 ;
2024-05-17 12:10:54 +00:00
// @todo Rename this regex to something more descriptive, ie: FILENAME_REGEX
2023-11-23 12:17:13 +00:00
public const regex = '([[:xdigit:]]{4})(?:-(\d{4,10}))?-(.+)' ;
2024-05-17 12:10:54 +00:00
/**
* Packet types we support , in specific order for auto - detection to work
*
* @ var string []
*/
2023-06-26 09:19:42 +00:00
public const PACKET_TYPES = [
'2.2' => FTNBase\Packet\FSC45 :: class ,
'2+' => FTNBase\Packet\FSC48 :: class ,
'2e' => FTNBase\Packet\FSC39 :: class ,
'2.0' => FTNBase\Packet\FTS1 :: class ,
2021-06-29 10:43:29 +00:00
];
2023-06-25 10:45:02 +00:00
protected array $header ; // Packet Header
2024-05-17 12:10:54 +00:00
protected ? string $name = NULL ; // Packet name
2021-07-15 14:54:23 +00:00
public File $file ; // Packet filename
2024-05-17 12:10:54 +00:00
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
2024-06-17 09:03:48 +00:00
protected string $content ; // Outgoing packet data
2021-08-13 13:46:48 +00:00
public Collection $errors ; // Messages that fail validation
2023-09-14 22:09:42 +00:00
protected int $index ; // Our array index
2024-06-17 09:03:48 +00:00
protected $pass_p = NULL ; // Overwrite the packet password (when packing messages)
2024-05-19 13:28:45 +00:00
/* 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 ;
2024-06-17 09:03:48 +00:00
abstract protected function header ( Collection $msgs ) : string ;
2021-08-24 13:42:03 +00:00
2023-06-25 10:45:02 +00:00
/* STATIC */
/**
2023-11-15 11:12:09 +00:00
* Size of the packet header
2023-06-25 10:45:02 +00:00
*
* @ return int
*/
public static function header_len () : int
2021-08-24 13:42:03 +00:00
{
2023-06-25 10:45:02 +00:00
return collect ( static :: HEADER ) -> sum ( function ( $item ) { return Arr :: get ( $item , 2 ); });
2021-08-24 13:42:03 +00:00
}
2021-07-15 14:54:23 +00:00
/**
2022-11-13 13:29:55 +00:00
* Process a packet file
2021-07-15 14:54:23 +00:00
*
2023-12-13 12:00:47 +00:00
* @ param mixed $f File handler returning packet data
2022-11-13 13:29:55 +00:00
* @ param string $name
* @ param int $size
2023-09-20 10:29:23 +00:00
* @ param Domain | null $domain
2021-07-15 14:54:23 +00:00
* @ return Packet
* @ throws InvalidPacketException
*/
2023-09-20 10:29:23 +00:00
public static function process ( mixed $f , string $name , int $size , Domain $domain = NULL ) : self
2021-07-15 14:54:23 +00:00
{
2022-11-13 13:29:55 +00:00
Log :: debug ( sprintf ( '%s:+ Opening Packet [%s] with size [%d]' , self :: LOGKEY , $name , $size ));
2021-07-15 14:54:23 +00:00
2023-06-25 10:45:02 +00:00
$o = FALSE ;
$header = '' ;
2022-11-13 13:29:55 +00:00
$read_ptr = 0 ;
2021-07-15 14:54:23 +00:00
2023-06-25 10:45:02 +00:00
// 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 )));
2021-07-15 14:54:23 +00:00
2023-06-25 10:45:02 +00:00
if ( $type :: is_type ( $header )) {
$o = new $type ( $header );
break ;
}
}
2021-07-15 14:54:23 +00:00
2023-06-25 10:45:02 +00:00
if ( ! $o )
throw new InvalidPacketException ( 'Cannot determine type of packet.' );
2021-07-15 14:54:23 +00:00
2022-11-13 13:29:55 +00:00
$o -> name = $name ;
2021-07-15 14:54:23 +00:00
$x = fread ( $f , 2 );
2023-12-13 12:00:47 +00:00
if ( strlen ( $x ) === 2 ) {
// End of Packet?
if ( $x === " \00 \00 " )
return $o ;
2021-07-15 14:54:23 +00:00
2023-12-13 12:00:47 +00:00
// Messages start with self::PACKED_MSG_LEAD
elseif ( $x !== self :: PACKED_MSG_LEAD )
throw new InvalidPacketException ( 'Not a valid packet: ' . bin2hex ( $x ));
2021-06-29 10:43:29 +00:00
2021-07-15 14:54:23 +00:00
// No message attached
2023-12-13 12:00:47 +00:00
} else
2024-05-17 12:10:54 +00:00
throw new InvalidPacketException ( 'Not a valid packet, not EOP or SOM:' . bin2hex ( $x ));
Log :: info ( sprintf ( '%s:- Packet [%s] is a [%s] packet, dated [%s]' , self :: LOGKEY , $o -> name , get_class ( $o ), $o -> date ));
2021-07-15 14:54:23 +00:00
2023-09-20 10:29:23 +00:00
// 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 ();
}
// If zone is not set, then we need to use a default zone - the messages may not be from this zone.
2024-05-17 12:10:54 +00:00
if ( empty ( $o -> zone )) {
2023-09-20 10:29:23 +00:00
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 ));
2021-11-24 11:34:40 +00:00
2023-09-20 10:29:23 +00:00
$o -> zone = Zone :: where ( 'zone_id' , $o -> fz )
-> where ( 'default' , TRUE )
-> singleOrFail ();
}
2021-08-29 13:58:12 +00:00
2023-12-07 09:19:48 +00:00
$message = '' ; // Current message we are building
2023-12-13 12:00:47 +00:00
$msgbuf = '' ;
2024-05-17 12:10:54 +00:00
$leader = Message :: header_len () + strlen ( self :: PACKED_MSG_LEAD );
2021-07-15 14:54:23 +00:00
2023-12-07 09:19:48 +00:00
// We loop through reading from the buffer, to find our end of message tag
2023-12-13 12:00:47 +00:00
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 ;
2024-05-17 12:10:54 +00:00
// If we have more to read
2023-12-13 12:00:47 +00:00
} elseif ( $read_ptr < $size ) {
continue ;
2021-08-26 12:01:16 +00:00
}
2023-12-13 12:00:47 +00:00
// If we get here
throw new InvalidPacketException ( sprintf ( 'Cannot determine END of message/packet: %s|%s' , get_class ( $o ), hex_dump ( $message )));;
2021-06-29 10:43:29 +00:00
}
2021-07-15 14:54:23 +00:00
2023-12-13 12:00:47 +00:00
if ( $msgbuf )
throw new InvalidPacketException ( sprintf ( 'Unprocessed data in packet: %s|%s' , get_class ( $o ), hex_dump ( $msgbuf )));
2021-07-30 14:35:52 +00:00
2021-07-15 14:54:23 +00:00
return $o ;
2021-06-29 10:43:29 +00:00
}
/**
2024-05-17 12:10:54 +00:00
* @ 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
2021-06-29 10:43:29 +00:00
*/
2024-05-17 12:10:54 +00:00
public function __get ( $key )
2021-06-29 10:43:29 +00:00
{
2024-05-20 11:31:21 +00:00
//Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key));
2024-05-17 12:10:54 +00:00
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 );
2024-05-28 02:23:59 +00:00
case 'product' :
return Arr :: get ( $this -> header , 'prodcode-hi' ) << 8 | Arr :: get ( $this -> header , 'prodcode-lo' );
2024-05-17 12:10:54 +00:00
case 'software' :
Software :: unguard ();
2024-05-28 02:23:59 +00:00
$o = Software :: singleOrNew ([ 'code' => $this -> product , 'type' => Software :: SOFTWARE_TOSSER ]);
2024-05-17 12:10:54 +00:00
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 ());
2024-05-19 13:28:45 +00:00
case 'messages' :
return $this -> { $key };
2024-05-17 12:10:54 +00:00
default :
throw new \Exception ( 'Unknown key: ' . $key );
}
2023-06-25 10:45:02 +00:00
}
2021-07-18 12:10:21 +00:00
2024-05-17 12:10:54 +00:00
/**
* Return the packet
*
* @ return string
* @ throws \Exception
*/
public function __toString () : string
2023-06-25 10:45:02 +00:00
{
2024-06-17 09:03:48 +00:00
return $this -> content ;
2021-06-29 10:43:29 +00:00
}
2023-06-25 10:45:02 +00:00
/* INTERFACE */
2021-07-15 14:54:23 +00:00
/**
2023-06-25 10:45:02 +00:00
* Number of messages in this packet
2021-07-15 14:54:23 +00:00
*/
2023-06-25 10:45:02 +00:00
public function count () : int
2021-06-29 10:43:29 +00:00
{
2023-06-25 10:45:02 +00:00
return $this -> messages -> count ();
}
2021-06-29 10:43:29 +00:00
2024-05-17 12:10:54 +00:00
public function current () : Echomail | Netmail
2023-06-25 10:45:02 +00:00
{
2023-09-14 22:09:42 +00:00
return $this -> messages -> get ( $this -> index );
2023-06-25 10:45:02 +00:00
}
2021-06-29 10:43:29 +00:00
2023-06-25 10:45:02 +00:00
public function key () : mixed
{
2023-09-14 22:09:42 +00:00
return $this -> index ;
2023-06-25 10:45:02 +00:00
}
2021-09-08 12:07:00 +00:00
2023-06-25 10:45:02 +00:00
public function next () : void
{
$this -> index ++ ;
2021-06-29 10:43:29 +00:00
}
2023-06-25 10:45:02 +00:00
public function rewind () : void
2021-06-29 10:43:29 +00:00
{
2023-06-25 10:45:02 +00:00
$this -> index = 0 ;
2021-06-29 10:43:29 +00:00
}
2023-06-25 10:45:02 +00:00
public function valid () : bool
2021-06-29 10:43:29 +00:00
{
2023-09-14 22:09:42 +00:00
return ( ! is_null ( $this -> key ())) && $this -> messages -> has ( $this -> key ());
2021-06-29 10:43:29 +00:00
}
2023-06-25 10:45:02 +00:00
/* METHODS */
2024-05-17 12:10:54 +00:00
public function for ( Address $ao ) : self
{
$this -> tftn_p = $ao ;
$this -> fftn_p = our_address ( $ao );
return $this ;
}
/**
* Generate a packet
*
* @ return string
*/
2024-05-19 13:28:45 +00:00
public function generate () : string
2024-05-17 12:10:54 +00:00
{
2024-05-19 13:28:45 +00:00
return ( string ) $this ;
}
2024-05-17 12:10:54 +00:00
2024-06-09 11:14:27 +00:00
public function mail ( Collection $msgs ) : self
2024-05-19 13:28:45 +00:00
{
2024-06-17 09:03:48 +00:00
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 " ;
2024-06-28 13:27:06 +00:00
$this -> messages = $msgs -> map ( fn ( $item ) => $item -> only ([ 'id' , 'date' ]));
2024-05-17 12:10:54 +00:00
2024-05-19 13:28:45 +00:00
return $this ;
2024-05-17 12:10:54 +00:00
}
2021-07-15 14:54:23 +00:00
/**
* Parse a message in a mail packet
*
* @ param string $message
2021-09-11 13:32:10 +00:00
* @ throws InvalidPacketException | \Exception
2021-07-15 14:54:23 +00:00
*/
2021-08-29 13:58:12 +00:00
private function parseMessage ( string $message ) : void
2021-07-15 14:54:23 +00:00
{
2023-09-20 10:29:23 +00:00
Log :: info ( sprintf ( '%s:+ Processing packet message [%d] bytes' , self :: LOGKEY , strlen ( $message )));
2021-12-29 02:44:27 +00:00
2021-08-29 13:58:12 +00:00
$msg = Message :: parseMessage ( $message , $this -> zone );
2021-07-19 14:26:12 +00:00
// If the message is invalid, we'll ignore it
2024-05-20 11:31:21 +00:00
if ( $msg -> errors -> count ()) {
Log :: info ( sprintf ( '%s:- Message [%s] has [%d] errors' , self :: LOGKEY , $msg -> msgid ? : 'No ID' , $msg -> errors -> count ()));
2023-09-20 10:29:23 +00:00
// If the messages is not for the right zone, we'll ignore it
2024-05-20 11:31:21 +00:00
if ( $msg -> errors -> has ( 'invalid-zone' )) {
2024-05-17 12:10:54 +00:00
Log :: alert ( sprintf ( '%s:! Message [%s] is from an invalid zone [%s], packet is from [%s] - ignoring it' , self :: LOGKEY , $msg -> msgid , $msg -> fftn -> zone -> zone_id , $this -> fftn -> zone -> zone_id ));
2023-09-20 10:29:23 +00:00
2024-05-21 11:29:21 +00:00
if ( ! $msg -> kludges -> get ( 'RESCANNED' ))
2024-05-17 12:10:54 +00:00
Notification :: route ( 'netmail' , $this -> fftn ) -> notify ( new EchomailBadAddress ( $msg ));
2023-09-20 10:29:23 +00:00
return ;
}
2024-05-20 11:31:21 +00:00
// 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 );
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 ));
2022-11-06 03:40:03 +00:00
return ;
}
2024-05-20 11:31:21 +00:00
if ( ! $ao ) {
$so = System :: createUnknownSystem ();
$ao = Address :: createFTN ( $msg -> set -> get ( 'set_fftn' ), $so );
}
2022-11-06 03:40:03 +00:00
2024-05-20 11:31:21 +00:00
$msg -> fftn_id = $ao -> id ;
Log :: alert ( sprintf ( '%s:- From FTN [%s] is not defined, created new entry for (%d)' , self :: LOGKEY , $msg -> set -> get ( 'set_fftn' ), $ao -> id ));
2022-11-06 03:40:03 +00:00
}
2024-05-20 11:31:21 +00:00
// 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 );
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 ));
2021-09-11 13:32:10 +00:00
return ;
}
2021-08-24 13:42:03 +00:00
2024-05-20 11:31:21 +00:00
if ( ! $ao ) {
$so = System :: createUnknownSystem ();
$ao = Address :: createFTN ( $msg -> set -> get ( 'set_fftn' ), $so );
}
2021-09-11 13:32:10 +00:00
2024-05-20 11:31:21 +00:00
$msg -> tftn_id = $ao -> id ;
Log :: alert ( sprintf ( '%s:- To FTN [%s] is not defined, created new entry for (%d)' , self :: LOGKEY , $msg -> set -> get ( 'set_tftn' ), $ao -> id ));
2022-11-06 03:40:03 +00:00
}
2021-09-12 12:09:45 +00:00
2024-05-20 11:31:21 +00:00
// 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 ));
2024-05-17 12:10:54 +00:00
2024-05-20 11:31:21 +00:00
$this -> messages -> push ( $msg );
2021-09-11 13:32:10 +00:00
return ;
2021-08-24 13:42:03 +00:00
}
2021-07-19 14:26:12 +00:00
}
2021-09-11 13:32:10 +00:00
2024-05-20 11:31:21 +00:00
// @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 ));
2023-09-14 22:09:42 +00:00
$this -> messages -> push ( $msg );
2021-06-29 10:43:29 +00:00
}
2023-12-13 12:00:47 +00:00
2024-05-19 13:28:45 +00:00
/**
* Overwrite the packet password
*
* @ param string | null $password
* @ return self
*/
public function password ( string $password = NULL ) : self
{
if ( $password && ( strlen ( $password ) < 9 ))
2024-06-14 05:09:04 +00:00
$this -> pass_p = strtoupper ( $password );
2024-05-19 13:28:45 +00:00
return $this ;
}
2021-06-29 10:43:29 +00:00
}