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; } }