Rework TIC processing and added test cases
This commit is contained in:
@@ -4,31 +4,30 @@ namespace App\Classes\FTN;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
|
||||
use App\Classes\FTN as FTNBase;
|
||||
use App\Models\{Address,File,Filearea,Setup,System};
|
||||
use App\Traits\EncodeUTF8;
|
||||
use App\Exceptions\{InvalidCRCException,
|
||||
InvalidPasswordException,
|
||||
NodeNotSubscribedException,
|
||||
NoWriteSecurityException};
|
||||
use App\Exceptions\TIC\{NoFileAreaException,NotToMeException,SizeMismatchException};
|
||||
use App\Models\{Address,File,Filearea,Setup};
|
||||
|
||||
/**
|
||||
* Class TIC
|
||||
* Used create the structure of TIC files
|
||||
* This class handles the TIC files that accompany file transfers
|
||||
*
|
||||
* @package App\Classes
|
||||
*/
|
||||
class Tic extends FTNBase
|
||||
{
|
||||
use EncodeUTF8;
|
||||
|
||||
private const LOGKEY = 'FT-';
|
||||
|
||||
private const cast_utf8 = [
|
||||
];
|
||||
|
||||
// Single value kludge items and whether they are required
|
||||
// http://ftsc.org/docs/fts-5006.001
|
||||
private array $_kludge = [
|
||||
@@ -53,30 +52,23 @@ class Tic extends FTNBase
|
||||
'pw' => FALSE, // Password
|
||||
];
|
||||
|
||||
private File $fo;
|
||||
private Filearea $area;
|
||||
private Collection $values;
|
||||
|
||||
private Address $origin; // Should be first address in Path
|
||||
private Address $from; // Should be last address in Path
|
||||
private File $file;
|
||||
private Address $to; // Should be me
|
||||
|
||||
public function __construct()
|
||||
public function __construct(File $file=NULL)
|
||||
{
|
||||
$this->fo = new File;
|
||||
$this->file = $file ?: new File;
|
||||
|
||||
$this->fo->kludges = collect();
|
||||
$this->fo->set_path = collect();
|
||||
$this->fo->set_seenby = collect();
|
||||
$this->fo->rogue_seenby = collect();
|
||||
|
||||
$this->values = collect();
|
||||
$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 'fo':
|
||||
case 'file':
|
||||
return $this->{$key};
|
||||
|
||||
default:
|
||||
@@ -85,45 +77,47 @@ class Tic extends FTNBase
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a TIC file for an address
|
||||
* Generate the TIC file
|
||||
*
|
||||
* @param Address $ao
|
||||
* @param File $fo
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function generate(Address $ao,File $fo): string
|
||||
public function __toString(): string
|
||||
{
|
||||
$sysaddress = Setup::findOrFail(config('app.id'))->system->match($ao->zone)->first();
|
||||
if (! $this->to)
|
||||
throw new \Exception('No to address defined');
|
||||
|
||||
$sysaddress = ($x=Setup::findOrFail(config('app.id'))->system)->match($this->to->zone)->first();
|
||||
|
||||
$result = collect();
|
||||
|
||||
// Origin is the first address in our path
|
||||
$result->put('ORIGIN',$fo->path->first()->ftn3d);
|
||||
$result->put('ORIGIN',$this->file->path->first()->ftn3d);
|
||||
$result->put('FROM',$sysaddress->ftn3d);
|
||||
$result->put('TO',$ao->ftn3d);
|
||||
$result->put('FILE',$fo->name);
|
||||
$result->put('SIZE',$fo->size);
|
||||
if ($fo->description)
|
||||
$result->put('DESC',$fo->description);
|
||||
if ($fo->replaces)
|
||||
$result->put('REPLACES',$fo->replaces);
|
||||
$result->put('AREA',$fo->filearea->name);
|
||||
$result->put('AREADESC',$fo->filearea->description);
|
||||
if ($x=$ao->session('ticpass'))
|
||||
$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->session('ticpass'))
|
||||
$result->put('PW',$x);
|
||||
$result->put('CRC',sprintf("%X",$fo->crc));
|
||||
$result->put('CRC',sprintf("%X",$this->file->crc));
|
||||
|
||||
$out = '';
|
||||
foreach ($result as $key=>$value)
|
||||
$out .= sprintf("%s %s\r\n",$key,$value);
|
||||
|
||||
foreach ($fo->path as $o)
|
||||
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 ($fo->seenby as $o)
|
||||
foreach ($this->file->seenby as $o)
|
||||
$out .= sprintf("SEENBY %s\r\n",$o->ftn3d);
|
||||
|
||||
$out .= sprintf("SEENBY %s\r\n",$sysaddress->ftn3d);
|
||||
@@ -140,103 +134,122 @@ class Tic extends FTNBase
|
||||
{
|
||||
Log::critical(sprintf('%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]',
|
||||
self::LOGKEY,
|
||||
$this->fo->nodelist_filearea_id,
|
||||
$this->fo->filearea->domain->filearea_id,
|
||||
str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),
|
||||
$this->fo->name,
|
||||
$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->fo->nodelist_filearea_id === $this->fo->filearea->domain->filearea_id)
|
||||
&& (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),$this->fo->name)));
|
||||
return (($this->file->nodelist_filearea_id === $this->file->filearea->domain->filearea_id)
|
||||
&& (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$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 void
|
||||
* @return File
|
||||
* @throws FileNotFoundException
|
||||
* @throws InvalidCRCException
|
||||
* @throws InvalidPasswordException
|
||||
* @throws NoFileAreaException
|
||||
* @throws NoWriteSecurityException
|
||||
* @throws NodeNotSubscribedException
|
||||
* @throws NotToMeException
|
||||
* @throws SizeMismatchException
|
||||
*/
|
||||
public function load(string $filename): void
|
||||
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 (str_contains($filename,'-')) {
|
||||
list($hex,$name) = explode('-',$filename);
|
||||
$hex = basename($hex);
|
||||
if (! $fs->exists($rel_path_name))
|
||||
throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$fs->path($rel_path_name)));
|
||||
|
||||
} else {
|
||||
$hex = '';
|
||||
}
|
||||
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)));
|
||||
|
||||
if (! $fs->exists($filename))
|
||||
throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$fs->path($filename)));
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
if (! is_readable($fs->path($filename)))
|
||||
throw new UnableToReadFile(sprintf('File [%s] is not readable',realpath($filename)));
|
||||
$aid = NULL;
|
||||
$mtime = NULL;
|
||||
$this->file->recv_tic = preg_replace('/\.[Tt][Ii][Cc]$/','',$filename);
|
||||
|
||||
$f = $fs->readStream($filename);
|
||||
if (! $f) {
|
||||
Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$filename));
|
||||
return;
|
||||
$m = [];
|
||||
if (preg_match('/^(([0-9A-F]{4})-)?(([0-9]{4,10})-)?(.*).[Tt][Ii][Cc]$/',$filename,$m)) {
|
||||
$aid = $m[2];
|
||||
$mtime = $m[4];
|
||||
$this->file->recv_tic = $m[5];
|
||||
}
|
||||
|
||||
$ldesc = '';
|
||||
|
||||
while (! feof($f)) {
|
||||
$line = chop(fgets($f));
|
||||
$matches = [];
|
||||
$m = [];
|
||||
|
||||
if (! $line)
|
||||
continue;
|
||||
|
||||
preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$matches);
|
||||
preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$m);
|
||||
|
||||
if (in_array(strtolower(Arr::get($matches,1,'-')),$this->_kludge)) {
|
||||
switch ($k=strtolower($matches[1])) {
|
||||
if (in_array(strtolower(Arr::get($m,1,'-')),$this->_kludge)) {
|
||||
switch ($k=strtolower($m[1])) {
|
||||
case 'area':
|
||||
$this->{$k} = Filearea::singleOrNew(['name'=>strtoupper($matches[2])]);
|
||||
try {
|
||||
if ($fo=Filearea::where('name',strtoupper($m[2]))->firstOrFail())
|
||||
$this->file->filearea_id = $fo->id;
|
||||
|
||||
break;
|
||||
|
||||
case 'origin':
|
||||
case 'from':
|
||||
case 'to':
|
||||
$this->{$k} = Address::findFTN($matches[2]);
|
||||
|
||||
if (! $this->{$k})
|
||||
Log::alert(sprintf('%s:! Unable to find an FTN for [%s] for the (%s)',self::LOGKEY,$matches[2],$k));
|
||||
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$this->fo->name = $matches[2];
|
||||
$this->fo->prefix = $hex;
|
||||
|
||||
if (! $fs->exists($this->fo->recvd_rel_name)) {
|
||||
// @todo Fail this, so that it is rescheduled to try again in 1-24hrs.
|
||||
|
||||
throw new FileNotFoundException(sprintf('File not found? [%s]',$fs->path($this->fo->recvd_rel_name)));
|
||||
} catch (ModelNotFoundException $e) {
|
||||
// Rethrow this as No File Area
|
||||
throw new NoFileAreaException($e);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'areadesc':
|
||||
$areadesc = $matches[2];
|
||||
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;
|
||||
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':
|
||||
// ignored
|
||||
break;
|
||||
|
||||
// This should be one of my addresses
|
||||
case 'to':
|
||||
$ftns = Setup::findOrFail(config('app.id'))->system->addresses->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 = $matches[2];
|
||||
$pw = $m[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'lfile':
|
||||
$this->fo->lname = $matches[2];
|
||||
case 'fullname':
|
||||
$this->file->lname = $m[2];
|
||||
|
||||
break;
|
||||
|
||||
@@ -244,91 +257,128 @@ class Tic extends FTNBase
|
||||
case 'magic':
|
||||
case 'replaces':
|
||||
case 'size':
|
||||
$this->fo->{$k} = $matches[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'fullname':
|
||||
$this->fo->lfile = $matches[2];
|
||||
$this->file->{$k} = $m[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
$this->fo->datetime = Carbon::createFromTimestamp($matches[2]);
|
||||
$this->file->datetime = Carbon::createFromTimestamp($m[2]);
|
||||
|
||||
break;
|
||||
|
||||
case 'ldesc':
|
||||
$ldesc .= ($ldesc ? "\r" : '').$matches[2];
|
||||
$ldesc .= ($ldesc ? "\r" : '').$m[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'crc':
|
||||
$this->fo->{$k} = hexdec($matches[2]);
|
||||
$this->file->{$k} = hexdec($m[2]);
|
||||
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
$this->fo->set_path->push($matches[2]);
|
||||
$this->file->set_path->push($m[2]);
|
||||
|
||||
break;
|
||||
|
||||
case 'seenby':
|
||||
$this->fo->set_seenby->push($matches[2]);
|
||||
$this->file->set_seenby->push($m[2]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->fo->kludges->push($line);
|
||||
$this->file->kludges->push($line);
|
||||
}
|
||||
}
|
||||
|
||||
if ($ldesc)
|
||||
$this->fo->ldesc = $ldesc;
|
||||
|
||||
fclose($f);
|
||||
|
||||
// @todo Add notifictions back to the system
|
||||
if ($this->fo->replaces && (! preg_match('/^'.$this->fo->replaces.'$/',$this->fo->name))) {
|
||||
Log::alert(sprintf('%s:! Regex [%s] doesnt match file name [%s]',self::LOGKEY,$this->fo->replaces,$this->fo->name));
|
||||
if ($ldesc)
|
||||
$this->file->ldesc = $ldesc;
|
||||
|
||||
$this->fo->replaces = NULL;
|
||||
// @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=$mtime-$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 ($this->fo->size !== ($y=$fs->size($this->fo->recvd_rel_name)))
|
||||
throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->fo->size,$this->fo->recvd_rel_name,$y));
|
||||
|
||||
// Validate CRC
|
||||
if (sprintf('%08x',$this->fo->crc) !== ($y=$fs->checksum($this->fo->recvd_rel_name,['checksum_algo'=>'crc32b'])))
|
||||
throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->fo->crc,$this->fo->recvd_rel_name,$y));
|
||||
if ($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 ($pw !== ($y=$this->from->session('ticpass')))
|
||||
throw new \Exception(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->from->ftn,$y));
|
||||
if ($pw !== ($y=$this->file->fftn->session('ticpass')))
|
||||
throw new InvalidPasswordException(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->file->fftn->ftn,$y));
|
||||
|
||||
// Validate Sender is linked (and permitted to send)
|
||||
if ($this->from->fileareas->search(function($item) { return $item->id === $this->area->id; }) === FALSE)
|
||||
throw new \Exception(sprintf('Node [%s] is not subscribed to [%s]',$this->from->ftn,$this->area->name));
|
||||
// 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));
|
||||
|
||||
// If the filearea is to be autocreated, create it
|
||||
if (! $this->area->exists) {
|
||||
$this->area->description = $areadesc;
|
||||
$this->area->active = TRUE;
|
||||
$this->area->show = FALSE;
|
||||
$this->area->notes = 'Autocreated';
|
||||
$this->area->domain_id = $this->from->zone->domain_id;
|
||||
$this->area->save();
|
||||
}
|
||||
|
||||
$this->fo->filearea_id = $this->area->id;
|
||||
$this->fo->fftn_id = $this->origin->id;
|
||||
// Validate sender is permitted to write
|
||||
// @todo Send a notification
|
||||
if (! $this->file->filearea->sec_write || ($this->file->fftn->security < $this->file->filearea->sec_write))
|
||||
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->fo->datetime)
|
||||
$this->fo->datetime = Carbon::createFromTimestamp($fs->lastModified($this->fo->recvd_rel_name));
|
||||
if (! $this->file->datetime)
|
||||
$this->file->datetime = Carbon::createFromTimestamp($fs->lastModified($file));
|
||||
|
||||
$this->fo->save();
|
||||
$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;
|
||||
}
|
||||
}
|
@@ -26,7 +26,7 @@ final class Tic extends Send
|
||||
$this->ftype = ((($type&0xff)<<8)|self::IS_TIC);
|
||||
$this->readpos = 0;
|
||||
|
||||
$this->tic = FTNTic::generate($ao,$file);
|
||||
$this->tic = (string)(new FTNTic($file))->to($ao);
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
|
Reference in New Issue
Block a user