clrghouz/app/Classes/FTN/Tic.php
Deon George b6a538f961
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
Optionally, also find TIC sender address in tic sending node's list of addresses
2024-11-19 20:32:45 +09:30

393 lines
11 KiB
PHP

<?php
namespace App\Classes\FTN;
use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToReadFile;
use App\Classes\FTN as FTNBase;
use App\Exceptions\{InvalidCRCException,
InvalidPasswordException,
NodeNotSubscribedException,
NoWriteSecurityException};
use App\Exceptions\TIC\{NoFileAreaException,NotToMeException,SizeMismatchException};
use App\Models\{Address,File,Filearea,Setup};
/**
* Class TIC
* This class handles the TIC files that accompany file transfers
*
* @package App\Classes
*/
class Tic extends FTNBase
{
private const LOGKEY = 'FT-';
// 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
];
private File $file;
private Address $to; // Should be me
public function __construct(File $file=NULL)
{
$this->file = $file ?: new File;
$this->file->kludges = collect();
$this->file->rogue_seenby = collect();
$this->file->set_path = collect();
$this->file->set_seenby = collect();
}
public function __get(string $key): mixed
{
switch ($key) {
case 'file':
return $this->{$key};
case 'name':
return $this->file->name;
default:
return parent::__get($key);
}
}
/**
* Generate the TIC file
*
* @return string
* @throws \Exception
*/
public function __toString(): string
{
if (! $this->to)
throw new \Exception('No to address defined');
$sysaddress = our_address($this->to);
$result = collect();
// Origin is the first address in our path
$result->put('ORIGIN',$this->file->path->first()->ftn3d);
$result->put('FROM',$sysaddress->ftn3d);
$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);
if ($x=$this->to->pass_tic)
$result->put('PW',$x);
$result->put('CRC',sprintf("%X",$this->file->crc));
$out = '';
foreach ($result as $key=>$value)
$out .= sprintf("%s %s\r\n",$key,$value);
foreach ($this->file->path as $o)
$out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra);
// Add ourself to the path:
$out .= sprintf("PATH %s %s\r\n",$sysaddress->ftn3d,Carbon::now());
foreach ($this->file->seenby as $o)
$out .= sprintf("SEENBY %s\r\n",$o->ftn3d);
$out .= sprintf("SEENBY %s\r\n",$sysaddress->ftn3d);
return $out;
}
/**
* Does this TIC file bring us a nodelist
*
* @return bool
*/
public function isNodelist(): bool
{
Log::info(sprintf('%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]',
self::LOGKEY,
$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,
));
return (($this->file->nodelist_filearea_id === $this->file->filearea->domain->filearea_id)
&& (preg_match(str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),$this->file->name)));
}
/**
* Load a TIC file from an existing filename
*
* @param string $filename Relative to filesystem
* @return File
* @throws FileNotFoundException
* @throws InvalidCRCException
* @throws InvalidPasswordException
* @throws NoFileAreaException
* @throws NoWriteSecurityException
* @throws NodeNotSubscribedException
* @throws NotToMeException
* @throws SizeMismatchException
*/
public function load(string $filename): File
{
Log::info(sprintf('%s:+ Processing TIC file [%s]',self::LOGKEY,$filename));
$fs = Storage::disk(config('fido.local_disk'));
$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 = [];
if (preg_match(sprintf('/^%s\.[Tt][Ii][Cc]$/',Packet::regex),$filename,$m)) {
$aid = $m[1];
$mtime = $m[2];
$this->file->recv_tic = $m[3];
}
$ldesc = '';
while (! feof($f)) {
$line = chop(fgets($f));
$m = [];
if (! $line)
continue;
preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$m);
if (in_array(strtolower(Arr::get($m,1,'-')),$this->_kludge)) {
switch ($k=strtolower($m[1])) {
case 'area':
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
throw new NoFileAreaException($e->getMessage());
}
break;
case 'from':
if (($ao=Address::findFTN($m[2])) && ((! $aid) || ($ao->zone->domain_id === Address::findOrFail(hexdec($aid))->zone->domain_id)))
$this->file->fftn_id = $ao->id;
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;
else
throw new ModelNotFoundException(sprintf('FTN Address [%s] not found or sender mismatch',$m[2]));
break;
// The origin should be the first address in the path
case 'origin':
// Ignore
case 'areadesc':
case 'created':
break;
// This should be one of my addresses
case 'to':
$ftns = our_address()->pluck('ftn3d');
if (! ($ftns->contains($m[2])))
throw new NotToMeException(sprintf('FTN Address [%s] not found or not one of my addresses',$m[2]));
break;
case 'file':
$this->file->name = $m[2];
break;
case 'pw':
$pw = $m[2];
break;
case 'lfile':
case 'fullname':
$this->file->lname = $m[2];
break;
case 'desc':
case 'magic':
case 'replaces':
case 'size':
$this->file->{$k} = $m[2];
break;
case 'date':
$this->file->datetime = Carbon::createFromTimestamp($m[2]);
break;
case 'ldesc':
$ldesc .= ($ldesc ? "\r" : '').$m[2];
break;
case 'crc':
$this->file->{$k} = hexdec($m[2]);
break;
case 'path':
$this->file->set_path->push($m[2]);
break;
case 'seenby':
$this->file->set_seenby->push($m[2]);
break;
}
} else {
$this->file->kludges->push($line);
}
}
fclose($f);
if ($ldesc)
$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 24hrs 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) {
if (abs($x=$fs->lastModified($rel_path_name)-$fs->lastModified($file)) > 86400) {
Log::debug(sprintf('%s:/ Ignoring [%s] its mtime is outside of our scope [%d]',self::LOGKEY,$file,$x));
continue;
}
// Our file should have the same prefix as the TIC file
if (preg_match('#/'.($aid ? $aid.'-' : '').'.*'.$this->file->name.'$#',$file)) {
$found = TRUE;
if (sprintf('%08x',$this->file->crc) === ($y=$fs->checksum($file,['checksum_algo'=>'crc32b']))) {
$crcOK = TRUE;
break;
}
}
}
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)));
// @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));
$this->file->replaces = NULL;
}
// @todo Add notification back to the system if no replaces line and the file already exists
// Validate Size
if ((! is_null($this->file->size)) && ($this->file->size !== ($y=$fs->size($file))))
throw new SizeMismatchException(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->file->size,$fs->path($rel_path_name),$y));
// Validate Password
if (strtoupper($pw) !== ($y=$this->file->fftn->pass_tic))
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
if (! $this->file->filearea->can_write($this->file->fftn->security))
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));
// If the file create time is blank, we'll take the files
if (! $this->file->datetime)
$this->file->datetime = Carbon::createFromTimestamp($fs->lastModified($file));
// 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);
$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;
return $this;
}
}