2022-11-01 11:24:36 +00:00
< ? php
namespace App\Classes\FTN ;
use Carbon\Carbon ;
use Illuminate\Contracts\Filesystem\FileNotFoundException ;
2023-11-21 23:40:15 +00:00
use Illuminate\Database\Eloquent\ModelNotFoundException ;
2023-09-23 12:01:18 +00:00
use Illuminate\Support\Arr ;
2022-11-01 11:24:36 +00:00
use Illuminate\Support\Facades\Log ;
2022-11-02 10:20:02 +00:00
use Illuminate\Support\Facades\Storage ;
2023-09-23 12:01:18 +00:00
use League\Flysystem\UnableToReadFile ;
2022-11-01 11:24:36 +00:00
use App\Classes\FTN as FTNBase ;
2023-11-21 23:40:15 +00:00
use App\Exceptions\ { InvalidCRCException ,
InvalidPasswordException ,
NodeNotSubscribedException ,
NoWriteSecurityException };
use App\Exceptions\TIC\ { NoFileAreaException , NotToMeException , SizeMismatchException };
use App\Models\ { Address , File , Filearea , Setup };
2022-11-01 11:24:36 +00:00
/**
* Class TIC
2023-11-21 23:40:15 +00:00
* This class handles the TIC files that accompany file transfers
2022-11-01 11:24:36 +00:00
*
* @ package App\Classes
*/
class Tic extends FTNBase
{
2022-11-15 11:01:05 +00:00
private const LOGKEY = 'FT-' ;
2022-11-01 11:24:36 +00:00
// Single value kludge items and whether they are required
// http://ftsc.org/docs/fts-5006.001
private array $_kludge = [
'AREA' => TRUE ,
'areadesc' => FALSE ,
'ORIGIN' => TRUE ,
'FROM' => TRUE ,
'to' => FALSE ,
'FILE' => TRUE , // 8.3 DOS format
'lfile' => FALSE , // alias fullname
'fullname' => FALSE ,
'size' => FALSE ,
'date' => FALSE , // File creation date
'desc' => FALSE , // One line description of file
'ldesc' => FALSE , // Can have multiple
'created' => FALSE ,
'magic' => FALSE ,
'replaces' => FALSE , // ? and * are wildcards, as per DOS
'CRC' => TRUE , // crc-32
'PATH' => TRUE , // can have multiple: [FTN] [unix timestamp] [datetime human readable] [signature]
'SEENBY' => TRUE ,
'pw' => FALSE , // Password
];
2023-11-21 23:40:15 +00:00
private File $file ;
2022-11-01 11:24:36 +00:00
private Address $to ; // Should be me
2023-11-21 23:40:15 +00:00
public function __construct ( File $file = NULL )
2023-06-22 07:36:22 +00:00
{
2023-11-21 23:40:15 +00:00
$this -> file = $file ? : new File ;
2023-06-22 07:36:22 +00:00
2023-11-21 23:40:15 +00:00
$this -> file -> kludges = collect ();
$this -> file -> rogue_seenby = collect ();
$this -> file -> set_path = collect ();
$this -> file -> set_seenby = collect ();
2023-06-22 07:36:22 +00:00
}
2023-09-05 09:57:34 +00:00
public function __get ( string $key ) : mixed
{
switch ( $key ) {
2023-11-21 23:40:15 +00:00
case 'file' :
2023-09-05 09:57:34 +00:00
return $this -> { $key };
2023-12-16 12:22:23 +00:00
case 'name' :
return $this -> file -> name ;
2023-09-05 09:57:34 +00:00
default :
return parent :: __get ( $key );
}
}
2023-06-22 07:36:22 +00:00
/**
2023-11-21 23:40:15 +00:00
* Generate the TIC file
2023-06-22 07:36:22 +00:00
*
* @ return string
2023-11-21 23:40:15 +00:00
* @ throws \Exception
2023-06-22 07:36:22 +00:00
*/
2023-11-21 23:40:15 +00:00
public function __toString () : string
2023-06-22 07:36:22 +00:00
{
2023-11-21 23:40:15 +00:00
if ( ! $this -> to )
throw new \Exception ( 'No to address defined' );
2024-04-21 11:40:55 +00:00
$sysaddress = our_address ( $this -> to );
2023-06-22 07:36:22 +00:00
$result = collect ();
// Origin is the first address in our path
2023-11-21 23:40:15 +00:00
$result -> put ( 'ORIGIN' , $this -> file -> path -> first () -> ftn3d );
2023-06-22 07:36:22 +00:00
$result -> put ( 'FROM' , $sysaddress -> ftn3d );
2023-11-21 23:40:15 +00:00
$result -> put ( 'TO' , $this -> to -> ftn3d );
$result -> put ( 'FILE' , $this -> file -> name );
$result -> put ( 'SIZE' , $this -> file -> size );
if ( $this -> file -> description )
$result -> put ( 'DESC' , $this -> file -> description );
if ( $this -> file -> replaces )
$result -> put ( 'REPLACES' , $this -> file -> replaces );
$result -> put ( 'AREA' , $this -> file -> filearea -> name );
$result -> put ( 'AREADESC' , $this -> file -> filearea -> description );
2024-06-14 05:09:04 +00:00
if ( $x = $this -> to -> pass_tic )
2023-07-17 06:36:53 +00:00
$result -> put ( 'PW' , $x );
2023-11-21 23:40:15 +00:00
$result -> put ( 'CRC' , sprintf ( " %X " , $this -> file -> crc ));
2023-06-22 07:36:22 +00:00
$out = '' ;
foreach ( $result as $key => $value )
$out .= sprintf ( " %s %s \r \n " , $key , $value );
2023-11-21 23:40:15 +00:00
foreach ( $this -> file -> path as $o )
2023-06-22 07:36:22 +00:00
$out .= sprintf ( " PATH %s %s %s \r \n " , $o -> ftn3d , $o -> pivot -> datetime , $o -> pivot -> extra );
2023-09-23 12:01:18 +00:00
// Add ourself to the path:
$out .= sprintf ( " PATH %s %s \r \n " , $sysaddress -> ftn3d , Carbon :: now ());
2023-11-21 23:40:15 +00:00
foreach ( $this -> file -> seenby as $o )
2023-06-22 07:36:22 +00:00
$out .= sprintf ( " SEENBY %s \r \n " , $o -> ftn3d );
2023-09-23 12:01:18 +00:00
$out .= sprintf ( " SEENBY %s \r \n " , $sysaddress -> ftn3d );
2023-06-22 07:36:22 +00:00
return $out ;
}
2022-11-01 11:24:36 +00:00
2023-07-17 06:36:53 +00:00
/**
* Does this TIC file bring us a nodelist
*
* @ return bool
*/
public function isNodelist () : bool
{
2024-11-19 07:07:29 +00:00
Log :: info ( sprintf ( '%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]' ,
2023-10-06 11:51:47 +00:00
self :: LOGKEY ,
2023-11-21 23:40:15 +00:00
$this -> file -> nodelist_filearea_id ,
$this -> file -> filearea -> domain -> filearea_id ,
str_replace ([ '.' , '?' ],[ '\.' , '[0-9]' ], '#^' . $this -> file -> filearea -> domain -> nodelist_filename . '$#i' ),
$this -> file -> name ,
2023-10-06 11:51:47 +00:00
));
2023-11-21 23:40:15 +00:00
return (( $this -> file -> nodelist_filearea_id === $this -> file -> filearea -> domain -> filearea_id )
2023-11-22 05:41:14 +00:00
&& ( preg_match ( str_replace ([ '.' , '?' ],[ '\.' , '[0-9]' ], '#^' . $this -> file -> filearea -> domain -> nodelist_filename . '$#i' ), $this -> file -> name )));
2023-07-17 06:36:53 +00:00
}
2023-06-22 07:36:22 +00:00
/**
* Load a TIC file from an existing filename
*
2023-09-23 12:01:18 +00:00
* @ param string $filename Relative to filesystem
2023-11-21 23:40:15 +00:00
* @ return File
2023-06-22 07:36:22 +00:00
* @ throws FileNotFoundException
2023-11-21 23:40:15 +00:00
* @ throws InvalidCRCException
* @ throws InvalidPasswordException
* @ throws NoFileAreaException
* @ throws NoWriteSecurityException
* @ throws NodeNotSubscribedException
* @ throws NotToMeException
* @ throws SizeMismatchException
2023-06-22 07:36:22 +00:00
*/
2023-11-21 23:40:15 +00:00
public function load ( string $filename ) : File
2023-06-22 07:36:22 +00:00
{
2023-09-08 11:11:53 +00:00
Log :: info ( sprintf ( '%s:+ Processing TIC file [%s]' , self :: LOGKEY , $filename ));
2023-09-23 12:01:18 +00:00
$fs = Storage :: disk ( config ( 'fido.local_disk' ));
2023-11-21 23:40:15 +00:00
$rel_path_name = sprintf ( '%s/%s' , config ( 'fido.dir' ), $filename );
if ( ! $fs -> exists ( $rel_path_name ))
throw new FileNotFoundException ( sprintf ( 'File [%s] doesnt exist' , $fs -> path ( $rel_path_name )));
if (( ! is_readable ( $fs -> path ( $rel_path_name ))) || ! ( $f = $fs -> readStream ( $rel_path_name )))
throw new UnableToReadFile ( sprintf ( 'File [%s] is not readable' , $fs -> path ( $rel_path_name )));
/*
* Filenames are in the format X - Y - N . tic
* Where :
* - X is the nodes address that sent us the file
* - Y is the mtime of the TIC file from the sender
* - N is the sender ' s filename
*/
$aid = NULL ;
$mtime = NULL ;
$this -> file -> recv_tic = preg_replace ( '/\.[Tt][Ii][Cc]$/' , '' , $filename );
$m = [];
2023-11-23 12:17:13 +00:00
if ( preg_match ( sprintf ( '/^%s\.[Tt][Ii][Cc]$/' , Packet :: regex ), $filename , $m )) {
$aid = $m [ 1 ];
$mtime = $m [ 2 ];
$this -> file -> recv_tic = $m [ 3 ];
2022-11-01 11:24:36 +00:00
}
2023-09-23 12:01:18 +00:00
$ldesc = '' ;
2022-11-01 11:24:36 +00:00
while ( ! feof ( $f )) {
$line = chop ( fgets ( $f ));
2023-11-21 23:40:15 +00:00
$m = [];
2022-11-01 11:24:36 +00:00
if ( ! $line )
continue ;
2023-11-21 23:40:15 +00:00
preg_match ( '/([a-zA-Z]+)\ ?(.*)?/' , $line , $m );
2022-11-01 11:24:36 +00:00
2023-11-21 23:40:15 +00:00
if ( in_array ( strtolower ( Arr :: get ( $m , 1 , '-' )), $this -> _kludge )) {
switch ( $k = strtolower ( $m [ 1 ])) {
2022-11-01 11:24:36 +00:00
case 'area' :
2023-11-21 23:40:15 +00:00
try {
if ( $fo = Filearea :: where ( 'name' , strtoupper ( $m [ 2 ])) -> firstOrFail ())
$this -> file -> filearea_id = $fo -> id ;
} catch ( ModelNotFoundException $e ) {
// Rethrow this as No File Area
2023-12-16 12:59:19 +00:00
throw new NoFileAreaException ( $e -> getMessage ());
2023-11-21 23:40:15 +00:00
}
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
case 'from' :
2023-11-21 23:40:15 +00:00
if (( $ao = Address :: findFTN ( $m [ 2 ])) && (( ! $aid ) || ( $ao -> zone -> domain_id === Address :: findOrFail ( hexdec ( $aid )) -> zone -> domain_id )))
$this -> file -> fftn_id = $ao -> id ;
2024-11-19 11:02:45 +00:00
elseif ( $aid && ( $x = Address :: findOrFail ( hexdec ( $aid ))) && (( $y = $x -> system -> akas -> search ( fn ( $item ) => str_starts_with ( $item -> ftn , $m [ 2 ]))) !== FALSE ))
$this -> file -> fftn_id = $x -> system -> akas -> get ( $y ) -> id ;
2023-11-21 23:40:15 +00:00
else
throw new ModelNotFoundException ( sprintf ( 'FTN Address [%s] not found or sender mismatch' , $m [ 2 ]));
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
2023-11-21 23:40:15 +00:00
// The origin should be the first address in the path
case 'origin' :
// Ignore
case 'areadesc' :
case 'created' :
break ;
2023-09-23 12:01:18 +00:00
2023-11-21 23:40:15 +00:00
// This should be one of my addresses
case 'to' :
2023-12-18 04:13:16 +00:00
$ftns = our_address () -> pluck ( 'ftn3d' );
2023-09-23 12:01:18 +00:00
2023-11-21 23:40:15 +00:00
if ( ! ( $ftns -> contains ( $m [ 2 ])))
throw new NotToMeException ( sprintf ( 'FTN Address [%s] not found or not one of my addresses' , $m [ 2 ]));
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
2023-11-21 23:40:15 +00:00
case 'file' :
$this -> file -> name = $m [ 2 ];
2023-06-22 07:36:22 +00:00
2022-11-01 11:24:36 +00:00
break ;
2023-06-22 07:36:22 +00:00
case 'pw' :
2023-11-21 23:40:15 +00:00
$pw = $m [ 2 ];
2023-09-23 12:01:18 +00:00
2023-09-05 09:57:34 +00:00
break ;
2023-06-22 07:36:22 +00:00
case 'lfile' :
2023-11-21 23:40:15 +00:00
case 'fullname' :
$this -> file -> lname = $m [ 2 ];
2023-09-23 12:01:18 +00:00
2023-09-05 09:57:34 +00:00
break ;
2023-06-23 07:33:47 +00:00
case 'desc' :
2022-11-01 11:24:36 +00:00
case 'magic' :
case 'replaces' :
2023-06-22 07:36:22 +00:00
case 'size' :
2023-11-21 23:40:15 +00:00
$this -> file -> { $k } = $m [ 2 ];
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
case 'date' :
2023-11-21 23:40:15 +00:00
$this -> file -> datetime = Carbon :: createFromTimestamp ( $m [ 2 ]);
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
case 'ldesc' :
2023-11-21 23:40:15 +00:00
$ldesc .= ( $ldesc ? " \r " : '' ) . $m [ 2 ];
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
case 'crc' :
2023-11-21 23:40:15 +00:00
$this -> file -> { $k } = hexdec ( $m [ 2 ]);
2023-09-23 12:01:18 +00:00
2022-11-01 11:24:36 +00:00
break ;
case 'path' :
2023-11-21 23:40:15 +00:00
$this -> file -> set_path -> push ( $m [ 2 ]);
2022-11-01 11:24:36 +00:00
break ;
case 'seenby' :
2023-11-21 23:40:15 +00:00
$this -> file -> set_seenby -> push ( $m [ 2 ]);
2022-11-01 11:24:36 +00:00
break ;
}
} else {
2023-11-21 23:40:15 +00:00
$this -> file -> kludges -> push ( $line );
2022-11-01 11:24:36 +00:00
}
}
2023-11-21 23:40:15 +00:00
fclose ( $f );
2023-09-23 12:01:18 +00:00
if ( $ldesc )
2023-11-21 23:40:15 +00:00
$this -> file -> ldesc = $ldesc ;
// @todo Check that origin is the first address in the path
// @todo Make sure origin/from are in seenby
// @todo Make sure origin/from are in the path
/*
* Find our file and check the CRC
* If there is more than 1 file , select files that within 24 hrs of the TIC file .
* If no files report file not found
* If there is more than 1 check each CRC to match the right one .
* If none match report , CRC error
*/
$found = FALSE ;
$crcOK = FALSE ;
foreach ( $fs -> files ( config ( 'fido.dir' )) as $file ) {
2023-11-22 06:25:48 +00:00
if ( abs ( $x = $fs -> lastModified ( $rel_path_name ) - $fs -> lastModified ( $file )) > 86400 ) {
2023-11-21 23:40:15 +00:00
Log :: debug ( sprintf ( '%s:/ Ignoring [%s] its mtime is outside of our scope [%d]' , self :: LOGKEY , $file , $x ));
2022-11-01 11:24:36 +00:00
2023-11-21 23:40:15 +00:00
continue ;
}
2022-11-01 11:24:36 +00:00
2023-11-21 23:40:15 +00:00
// Our file should have the same prefix as the TIC file
if ( preg_match ( '#/' . ( $aid ? $aid . '-' : '' ) . '.*' . $this -> file -> name . '$#' , $file )) {
$found = TRUE ;
2023-09-23 12:01:18 +00:00
2023-11-21 23:40:15 +00:00
if ( sprintf ( '%08x' , $this -> file -> crc ) === ( $y = $fs -> checksum ( $file ,[ 'checksum_algo' => 'crc32b' ]))) {
$crcOK = TRUE ;
break ;
}
}
2023-09-23 12:01:18 +00:00
}
2023-11-21 23:40:15 +00:00
if (( $found ) && ( ! $crcOK ))
throw new InvalidCRCException ( sprintf ( 'TIC file CRC [%08x] doesnt match file [%s] (%s)' , $this -> file -> crc , $fs -> path ( $rel_path_name ), $y ));
elseif ( ! $found )
throw new FileNotFoundException ( sprintf ( 'File not found? [%s...%s] in [%s]' , $aid , $this -> file -> name , $fs -> path ( $rel_path_name )));
2022-11-01 11:24:36 +00:00
2023-11-21 23:40:15 +00:00
// @todo Add notifications back to the system if the replaces line doesnt match
if ( $this -> file -> replaces && ( ! preg_match ( '/^' . $this -> file -> replaces . '$/' , $this -> file -> name ))) {
Log :: alert ( sprintf ( '%s:! Regex [%s] doesnt match file name [%s]' , self :: LOGKEY , $this -> file -> replaces , $this -> file -> name ));
2022-11-01 11:24:36 +00:00
2023-11-21 23:40:15 +00:00
$this -> file -> replaces = NULL ;
2022-11-01 11:24:36 +00:00
}
2023-11-21 23:40:15 +00:00
// @todo Add notification back to the system if no replaces line and the file already exists
// Validate Size
2024-11-19 07:07:29 +00:00
if (( ! is_null ( $this -> file -> size )) && ( $this -> file -> size !== ( $y = $fs -> size ( $file ))))
2023-11-21 23:40:15 +00:00
throw new SizeMismatchException ( sprintf ( 'TIC file size [%d] doesnt match file [%s] (%d)' , $this -> file -> size , $fs -> path ( $rel_path_name ), $y ));
// Validate Password
2024-06-14 05:09:04 +00:00
if ( strtoupper ( $pw ) !== ( $y = $this -> file -> fftn -> pass_tic ))
2023-11-21 23:40:15 +00:00
throw new InvalidPasswordException ( sprintf ( 'TIC file PASSWORD [%s] doesnt match system [%s] (%s)' , $pw , $this -> file -> fftn -> ftn , $y ));
// Validate Sender is linked
if ( $this -> file -> fftn -> fileareas -> search ( function ( $item ) { return $item -> id === $this -> file -> filearea_id ; }) === FALSE )
throw new NodeNotSubscribedException ( sprintf ( 'Node [%s] is not subscribed to [%s]' , $this -> file -> fftn -> ftn , $this -> file -> filearea -> name ));
// Validate sender is permitted to write
// @todo Send a notification
2024-04-14 11:16:33 +00:00
if ( ! $this -> file -> filearea -> can_write ( $this -> file -> fftn -> security ))
2023-11-21 23:40:15 +00:00
throw new NoWriteSecurityException ( sprintf ( 'Node [%s] doesnt have enough security to write to [%s] (%d)' , $this -> file -> fftn -> ftn , $this -> file -> filearea -> name , $this -> file -> fftn -> security ));
2022-11-01 11:24:36 +00:00
// If the file create time is blank, we'll take the files
2023-11-21 23:40:15 +00:00
if ( ! $this -> file -> datetime )
$this -> file -> datetime = Carbon :: createFromTimestamp ( $fs -> lastModified ( $file ));
2024-11-19 07:07:29 +00:00
// If the file size was omitted, we'll use the file's size
if ( is_null ( $this -> file -> size ))
$this -> file -> size = $fs -> size ( $file );
2023-11-21 23:40:15 +00:00
$this -> file -> src_file = $file ;
$this -> file -> recv_tic = $filename ;
return $this -> file ;
}
public function save () : bool
{
return $this -> file -> save ();
}
public function to ( Address $ao ) : self
{
$this -> to = $ao ;
2022-11-01 11:24:36 +00:00
2023-11-21 23:40:15 +00:00
return $this ;
2022-11-01 11:24:36 +00:00
}
}