diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index c924b6d..e763a75 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -113,23 +113,26 @@ class Packet extends FTNBase implements \Iterator, \Countable } /** - * Open a packet file + * Process a packet file * - * @param File $file + * @param mixed $f + * @param string $name + * @param int $size * @param System|null $system * @param bool $use_cache * @return Packet * @throws InvalidPacketException */ - public static function open(File $file,System $system=NULL,bool $use_cache=TRUE): self - { - Log::debug(sprintf('%s:+ Opening Packet [%s]',self::LOGKEY,$file)); - $f = fopen($file,'r'); - $fstat = fstat($f); + public static function process(mixed $f,string $name,int $size,System $system=NULL,bool $use_cache=TRUE): self + { + Log::debug(sprintf('%s:+ Opening Packet [%s] with size [%d]',self::LOGKEY,$name,$size)); + + $read_ptr = 0; // PKT Header $header = fread($f,self::HEADER_LEN); + $read_ptr += strlen($header); // Could not read header if (strlen($header) != self::HEADER_LEN) @@ -142,10 +145,11 @@ class Packet extends FTNBase implements \Iterator, \Countable $o = new self; $o->use_cache = $use_cache; - $o->name = (string)$file; + $o->name = $name; $o->header = unpack(self::unpackheader(self::v2header),$header); $x = fread($f,2); + $read_ptr += strlen($x); // End of Packet? if (strlen($x) == 2 and $x == "\00\00") @@ -171,6 +175,8 @@ class Packet extends FTNBase implements \Iterator, \Countable $last = ''; while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) { + $read_ptr += strlen($readbuf); + if (strlen($message) < self::PACKED_MSG_HEADER_LEN) { $addchars = self::PACKED_MSG_HEADER_LEN-strlen($message); $message .= substr($readbuf,$buf_ptr,$addchars); @@ -204,7 +210,7 @@ class Packet extends FTNBase implements \Iterator, \Countable // In case our packet break is at the end of the buffer $last = substr($readbuf,-2); - if ((str_contains($last,"\x00")) && ($fstat['size']-ftell($f) > 2)) { + if ((str_contains($last,"\x00")) && ($size-$read_ptr > 2)) { $message .= substr($readbuf,$buf_ptr); $buf_ptr = 0; @@ -216,7 +222,7 @@ class Packet extends FTNBase implements \Iterator, \Countable } // See if we have found the end of the packet, if not read more. - if ($end === FALSE && (ftell($f) < $fstat['size'])) { + if ($end === FALSE && ($read_ptr < $size)) { $message .= substr($readbuf,$buf_ptr); $buf_ptr = 0; diff --git a/app/Classes/File.php b/app/Classes/File.php new file mode 100644 index 0000000..d95a509 --- /dev/null +++ b/app/Classes/File.php @@ -0,0 +1,92 @@ +guessExtension()) { + case 'zip': + $this->canHandle = TRUE; + $this->isArchive = TRUE; + $this->z = new \ZipArchive; + $this->z->open($this->getRealPath()); + break; + + case 'bin': + if ($this->getExtension() == 'pkt' || ($path instanceof UploadedFile && $path->getClientOriginalExtension() == 'pkt')) { + $this->canHandle = TRUE; + break; + }; + + default: + Log::alert(sprintf('%s:? Unknown file received: %s',self::LOGKEY,$x)); + } + } + + /* ITERATOR */ + + public function current() + { + if ($this->isArchive) { + $this->zipfile = $this->z->statIndex($this->counter,\ZipArchive::FL_UNCHANGED); + + $f = $this->z->getStream($this->zipfile['name']); + if (! $f) + throw new \Exception(sprintf('%s:Failed getting ZipArchive::stream (%s)',self::LOGKEY,$this->z->getStatusString())); + + return $f; + + } else { + return fopen($this->getRealPath(),'r+'); + } + } + + public function next() + { + $this->counter++; + } + + public function key() + { + return $this->counter; + } + + public function valid() + { + // If we have a pkt file, then counter can only be 1. + return $this->canHandle && (($this->isArchive && ($this->counter < $this->z->numFiles)) || $this->counter === 0); + } + + public function rewind() + { + $this->counter = 0; + } + + /* METHODS */ + + public function itemName(): string + { + return ($this->isArchive && $this->valid()) ? Arr::get(stream_get_meta_data($this->current()),'uri') : $this->getFilename(); + } + + public function itemSize(): int + { + return $this->isArchive ? Arr::get($this->zipfile,'size') : $this->getSize(); + } +} \ No newline at end of file diff --git a/app/Classes/File/Receive.php b/app/Classes/File/Receive.php index 27448da..95df9e9 100644 --- a/app/Classes/File/Receive.php +++ b/app/Classes/File/Receive.php @@ -3,13 +3,13 @@ namespace App\Classes\File; use Exception; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\Exception\FileException; -use App\Classes\FTN\InvalidPacketException; -use App\Classes\FTN\Packet; +use App\Classes\File; +use App\Classes\FTN\{InvalidPacketException,Packet}; use App\Jobs\{MessageProcess,TicProcess}; use App\Models\Address; @@ -54,7 +54,7 @@ final class Receive extends Item case 'mtime': case 'name': case 'size': - return $this->receiving ? $this->receiving->{'file_'.$key} : NULL; + return $this->receiving?->{'file_'.$key}; case 'name_size_time': return sprintf('%s %lu %lu',$this->name,$this->size,$this->mtime); @@ -108,64 +108,67 @@ final class Receive extends Item // If we received a packet, we'll dispatch a job to process it if (! $this->receiving->incomplete) switch ($this->receiving->file_type) { + case self::IS_ARC: case self::IS_PKT: - Log::info(sprintf('%s: - Processing mail packet [%s]',self::LOGKEY,$this->file)); + Log::info(sprintf('%s: - Processing mail %s [%s]',self::LOGKEY,$this->receiving->file_type === self::IS_PKT ? 'PACKET' : 'ARCHIVE',$this->file)); try { - $po = Packet::open(new File($this->file),$this->ao->system); + $f = new File($this->file); + + foreach ($f as $packet) { + $po = Packet::process($packet,Arr::get(stream_get_meta_data($packet),'uri'),$f->itemSize(),$this->ao->system); + + // Check the messages are from the uplink + if ($this->ao->system->addresses->search(function($item) use ($po) { return $item->id == $po->fftn_o->id; }) === FALSE) { + Log::error(sprintf('%s: ! Packet [%s] is not from this link? [%d]',self::LOGKEY,$po->fftn_o->ftn,$this->ao->system_id)); + break; + } + + // Check the packet password + if ($this->ao->session('pktpass') != $po->password) { + Log::error(sprintf('%s: ! Packet from [%s] with password [%s] is invalid.',self::LOGKEY,$this->ao->ftn,$po->password)); + // @todo Generate message to system advising invalid password - that message should be sent without a packet password! + break; + } + + Log::info(sprintf('%s: - Packet has [%d] messages',self::LOGKEY,$po->count())); + + // Queue messages if there are too many in the packet. + if ($queue = ($po->count() > config('app.queue_msgs'))) + Log::info(sprintf('%s: - Messages will be sent to the queue for processing',self::LOGKEY)); + + $error = FALSE; + foreach ($po as $msg) { + Log::info(sprintf('%s: - Mail from [%s] to [%s]',self::LOGKEY,$msg->fftn,$msg->tftn)); + + // @todo Quick check that the packet should be processed by us. + // @todo validate that the packet's zone is in the domain. + + try { + // Dispatch job. + if ($queue) + MessageProcess::dispatch($msg); + else + MessageProcess::dispatchSync($msg); + + } catch (Exception $e) { + Log::error(sprintf('%s:! Got error dispatching message [%s] (%d:%s-%s).',self::LOGKEY,$msg->msgid,$e->getLine(),$e->getFile(),$e->getMessage())); + $error = TRUE; + } + } + + if ($po->errors->count() || $error) { + Log::info(sprintf('%s: - Not deleting packet [%s], as it has validation errors',self::LOGKEY,$this->file)); + + // If we want to keep the packet, we could do that logic here + } elseif (! config('app.packet_keep')) { + Log::debug(sprintf('%s: - Deleting processed packet [%s]',self::LOGKEY,$this->file)); + unlink($this->file); + } + } } catch (InvalidPacketException $e) { Log::error(sprintf('%s: - Not deleting packet [%s], as it generated an exception',self::LOGKEY,$this->file)); - - break; - } - - // Check the messages are from the uplink - if ($this->ao->system->addresses->search(function($item) use ($po) { return $item->id == $po->fftn_o->id; }) === FALSE) { - Log::error(sprintf('%s: ! Packet [%s] is not from this link? [%d]',self::LOGKEY,$po->fftn_o->ftn,$this->ao->system_id)); - break; - } - - // Check the packet password - if ($this->ao->session('pktpass') != $po->password) { - Log::error(sprintf('%s: ! Packet from [%s] with password [%s] is invalid.',self::LOGKEY,$this->ao->ftn,$po->password)); - // @todo Generate message to system advising invalid password - that message should be sent without a packet password! - break; - } - - Log::info(sprintf('%s: - Packet has [%d] messages',self::LOGKEY,$po->count())); - - // Queue messages if there are too many in the packet. - if ($queue = ($po->count() > config('app.queue_msgs'))) - Log::info(sprintf('%s: - Messages will be sent to the queue for processing',self::LOGKEY)); - - $error = FALSE; - foreach ($po as $msg) { - Log::info(sprintf('%s: - Mail from [%s] to [%s]',self::LOGKEY,$msg->fftn,$msg->tftn)); - - // @todo Quick check that the packet should be processed by us. - // @todo validate that the packet's zone is in the domain. - - try { - // Dispatch job. - if ($queue) - MessageProcess::dispatch($msg); - else - MessageProcess::dispatchSync($msg); - - } catch (Exception $e) { - Log::error(sprintf('%s:! Got error dispatching message [%s] (%d:%s-%s).',self::LOGKEY,$msg->msgid,$e->getLine(),$e->getFile(),$e->getMessage())); - $error = TRUE; - } - } - - if ($po->errors->count() || $error) { - Log::info(sprintf('%s: - Not deleting packet [%s], as it has validation errors',self::LOGKEY,$this->file)); - - // If we want to keep the packet, we could do that logic here - } elseif (! config('app.packet_keep')) { - Log::debug(sprintf('%s: - Deleting processed packet [%s]',self::LOGKEY,$this->file)); - unlink($this->file); } break; diff --git a/app/Console/Commands/PacketInfo.php b/app/Console/Commands/PacketInfo.php index b017ec1..5b47b4e 100644 --- a/app/Console/Commands/PacketInfo.php +++ b/app/Console/Commands/PacketInfo.php @@ -3,8 +3,9 @@ namespace App\Console\Commands; use Illuminate\Console\Command; -use Symfony\Component\HttpFoundation\File\File; +use Illuminate\Support\Arr; +use App\Classes\File; use App\Classes\FTN\Packet; use App\Models\System; @@ -16,7 +17,7 @@ class PacketInfo extends Command * @var string */ protected $signature = 'packet:info' - .' {pkt : Packet to process}' + .' {file : Packet to process}' .' {system? : Zone the packet is from}'; /** @@ -34,42 +35,48 @@ class PacketInfo extends Command */ public function handle() { - $f = new File($this->argument('pkt')); + $f = new File($this->argument('file')); $s = $this->argument('system') ? System::where('name',$this->argument('zone'))->singleOrFail() : NULL; - $pkt = Packet::open($f,$s); + foreach ($f as $packet) { + $pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$s); - $this->info(sprintf('Packet Type: %s',$pkt->type)); - $this->info(sprintf('From: %s to %s',$pkt->fftn,$pkt->tftn)); - $this->info(sprintf('Dated: %s',$pkt->date)); - $this->info(sprintf('Password: %s (%s)',$pkt->password,$pkt->password ? 'SET' : 'NOT set')); - $this->info(sprintf('Messages: %d',$pkt->messages->count())); - $this->info(sprintf('Tosser %d (%s) version %s',$pkt->software->code,$pkt->software->name,$pkt->software_ver)); - $this->info(sprintf('Capabilities: %x',$pkt->capability)); - $this->info(sprintf('Has Errors: %s',$pkt->errors->count() ? 'YES' : 'No')); - $this->info(sprintf('Messages: %d',$pkt->count())); + $this->alert(sprintf('File Name: %s',$x)); - foreach ($pkt as $msg) { - $this->warn(sprintf('- Date: %s',$msg->date)); - $this->warn(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', '))); - $this->warn(sprintf(' - From: %s (%s)',$msg->user_from,$msg->fftn)); - $this->warn(sprintf(' - To: %s (%s)',$msg->user_to,$msg->tftn)); - $this->warn(sprintf(' - Subject: %s',$msg->subject)); + $this->info(sprintf('Packet Type: %s',$pkt->type)); + $this->info(sprintf('From: %s to %s',$pkt->fftn,$pkt->tftn)); + $this->info(sprintf('Dated: %s',$pkt->date)); + $this->info(sprintf('Password: %s (%s)',$pkt->password,$pkt->password ? 'SET' : 'NOT set')); + $this->info(sprintf('Messages: %d',$pkt->messages->count())); + $this->info(sprintf('Tosser %d (%s) version %s',$pkt->software->code,$pkt->software->name,$pkt->software_ver)); + $this->info(sprintf('Capabilities: %x',$pkt->capability)); + $this->info(sprintf('Has Errors: %s',$pkt->errors->count() ? 'YES' : 'No')); + $this->info(sprintf('Messages: %d',$pkt->count())); + + foreach ($pkt as $msg) { + $this->warn(sprintf('- Date: %s',$msg->date)); + $this->warn(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', '))); + $this->warn(sprintf(' - From: %s (%s)',$msg->user_from,$msg->fftn)); + $this->warn(sprintf(' - To: %s (%s)',$msg->user_to,$msg->tftn)); + $this->warn(sprintf(' - Subject: %s',$msg->subject)); + + if ($msg->errors) + foreach ($msg->errors->errors()->all() as $error) + $this->line(' - '.$error); + } + + foreach ($pkt->errors as $msg) { + $this->error(sprintf('- Date: %s',$msg->date)); + $this->error(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', '))); + $this->error(sprintf(' - From: %s (%s)',$msg->user_from,$msg->fftn)); + $this->error(sprintf(' - To: %s (%s)',$msg->user_to,$msg->tftn)); + $this->error(sprintf(' - Subject: %s',$msg->subject)); - if ($msg->errors) foreach ($msg->errors->errors()->all() as $error) $this->line(' - '.$error); - } + } - foreach ($pkt->errors as $msg) { - $this->error(sprintf('- Date: %s',$msg->date)); - $this->error(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', '))); - $this->error(sprintf(' - From: %s (%s)',$msg->user_from,$msg->fftn)); - $this->error(sprintf(' - To: %s (%s)',$msg->user_to,$msg->tftn)); - $this->error(sprintf(' - Subject: %s',$msg->subject)); - - foreach ($msg->errors->errors()->all() as $error) - $this->line(' - '.$error); + $this->line("\n"); } } } diff --git a/app/Console/Commands/PacketProcess.php b/app/Console/Commands/PacketProcess.php index 3ce8317..787824c 100644 --- a/app/Console/Commands/PacketProcess.php +++ b/app/Console/Commands/PacketProcess.php @@ -3,8 +3,9 @@ namespace App\Console\Commands; use Illuminate\Console\Command; -use Symfony\Component\HttpFoundation\File\File; +use Illuminate\Support\Arr; +use App\Classes\File; use App\Classes\FTN\Packet; use App\Jobs\MessageProcess as Job; use App\Models\Address; @@ -17,7 +18,7 @@ class PacketProcess extends Command * @var string */ protected $signature = 'packet:process' - .' {pkt : Packet to process}' + .' {file : Packet to process}' .' {--N|nobot : Dont process bots}' .' {ftn? : System the packet is from}'; @@ -36,17 +37,19 @@ class PacketProcess extends Command */ public function handle() { - $f = new File($this->argument('pkt')); + $f = new File($this->argument('file')); $s = $this->argument('ftn') ? Address::findFTN($this->argument('ftn'))->system : NULL; - foreach (Packet::open($f,$s) as $msg) { - // @todo Quick check that the packet should be processed by us. - // @todo validate that the packet's zone is in the domain. + foreach ($f as $packet) { + foreach (Packet::process($packet,$f->itemName(),$f->itemSize(),$s) as $msg) { + // @todo Quick check that the packet should be processed by us. + // @todo validate that the packet's zone is in the domain. - $this->info(sprintf('Processing message from [%s] with msgid [%s]',$msg->fboss,$msg->msgid)); + $this->info(sprintf('Processing message from [%s] with msgid [%s]',$msg->fboss,$msg->msgid)); - // Dispatch job. - Job::dispatchSync($msg,$this->option('nobot')); + // Dispatch job. + Job::dispatchSync($msg,$this->option('nobot')); + } } } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index f440d0f..0ca1440 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; +use App\Classes\File; use App\Classes\FTN\Packet; use App\Models\{Address,Domain,Echomail,Setup}; @@ -46,8 +47,9 @@ class HomeController extends Controller */ public function pkt(Request $request) { - $pkt = NULL; + $pkt = collect(); $file = NULL; + $f = NULL; if ($request->post()) { $request->validate([ @@ -57,10 +59,12 @@ class HomeController extends Controller foreach ($request->allFiles() as $key => $filegroup) { if ($key !== 'file') continue; - foreach ($filegroup as $file) { try { - $pkt = Packet::open($file); + $f = new File($file); + + foreach ($f as $packet) + $pkt->push([$f->itemName()=>Packet::process($packet,$f->itemName(),$f->itemSize())]); } catch (\Exception $e) { return redirect()->back()->withErrors(sprintf('%s (%s:%d)',$e->getMessage(),$e->getFile(),$e->getLine())); @@ -72,8 +76,9 @@ class HomeController extends Controller } return view('pkt') - ->with('file',$file) - ->with('result',$pkt) + ->with('file',$f) + ->with('filename',$f ? $file->getClientOriginalName() : NULL) + ->with('results',$pkt) ->with('hexdump',$file ? hex_dump(file_get_contents($file)) : ''); } diff --git a/app/Traits/Import.php b/app/Traits/Import.php index d5ce711..683df55 100644 --- a/app/Traits/Import.php +++ b/app/Traits/Import.php @@ -36,6 +36,7 @@ trait Import return $c; } + // @todo Consider merging this with File::openZipFile private function openFile(string $file,&$f): \ZipArchive { $z = new \ZipArchive; diff --git a/resources/views/pkt.blade.php b/resources/views/pkt.blade.php index fe05823..0c0a344 100644 --- a/resources/views/pkt.blade.php +++ b/resources/views/pkt.blade.php @@ -53,109 +53,126 @@ - @if($result) -
Packet {{ $file->getClientOriginalName() }} (type {{ $result->type }}) is from {{ $result->fftn }} to {{ $result->tftn }}, dated {{ $result->date }}.
-This packet has {{ $result->messages->count() }} messages and {{ $result->password ? 'DOES' : 'does NOT' }} have a password.
-Tosser: {{ $result->software->code }} ({{ $result->software->name }}), version {{ $result->software_ver }}. Capabilities: {{ $result->capability }}.
- @if ($result->messages->count() > 1) -You can expand each one
- @endif -