Support for ZIP archives

This commit is contained in:
Deon George 2022-11-14 00:29:55 +11:00
parent 702a4e4f37
commit 3ffb1c1fd6
9 changed files with 441 additions and 295 deletions

View File

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

92
app/Classes/File.php Normal file
View File

@ -0,0 +1,92 @@
<?php
namespace App\Classes;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\File as FileBase;
class File extends FileBase implements \Iterator
{
private const LOGKEY = 'F--';
private int $counter = 0;
private bool $isArchive = FALSE;
private bool $canHandle = FALSE;
private \ZipArchive $z;
private array $zipfile = [];
public function __construct(mixed $path,bool $checkPath=true)
{
parent::__construct($path,$checkPath);
switch($x=$this->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();
}
}

View File

@ -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,17 +108,15 @@ 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);
} catch (InvalidPacketException $e) {
Log::error(sprintf('%s: - Not deleting packet [%s], as it generated an exception',self::LOGKEY,$this->file));
break;
}
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) {
@ -167,6 +165,11 @@ final class Receive extends Item
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;

View File

@ -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,10 +35,13 @@ 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->alert(sprintf('File Name: %s',$x));
$this->info(sprintf('Packet Type: %s',$pkt->type));
$this->info(sprintf('From: %s to %s',$pkt->fftn,$pkt->tftn));
@ -71,5 +75,8 @@ class PacketInfo extends Command
foreach ($msg->errors->errors()->all() as $error)
$this->line(' - '.$error);
}
$this->line("\n");
}
}
}

View File

@ -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,10 +37,11 @@ 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) {
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.
@ -50,3 +52,4 @@ class PacketProcess extends Command
}
}
}
}

View File

@ -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)) : '');
}

View File

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

View File

@ -53,24 +53,31 @@
</div>
</form>
@if($result)
<h3>Packet Results</h3>
<p>Packet <strong class="highlight">{{ $file->getClientOriginalName() }}</strong> (type <strong class="highlight">{{ $result->type }}</strong>) is from <strong class="highlight">{{ $result->fftn }}</strong> to <strong class="highlight">{{ $result->tftn }}</strong>, dated <strong class="highlight">{{ $result->date }}</strong>.</p>
@if($results->count())
<h2>Archive Results: <strong class="highlight">{{ $filename }}</strong></h2>
<div class="accordion accordion-flush" id="accordion_packet">
@foreach($results as $item)
@foreach ($item as $file => $result)
<h3 class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse_item_{{ $loop->parent->index }}_{{ $loop->index }}" aria-expanded="false">Packet Results <strong class="highlight">{{ $file }}</strong></h3>
<div id="collapse_item_{{ $loop->parent->index }}_{{ $loop->index }}" class="accordion-collapse collapse @if($result->messages->count() == 1 && $loop->parent->first)show @endif" aria-labelledby="packetdebug" data-bs-parent="#accordion_packet">
<div class="accordion-body">
<p>Packet <strong class="highlight">{{ $file }}</strong> (type <strong class="highlight">{{ $result->type }}</strong>) is from <strong class="highlight">{{ $result->fftn }}</strong> to <strong class="highlight">{{ $result->tftn }}</strong>, dated <strong class="highlight">{{ $result->date }}</strong>.</p>
<p>This packet has <strong class="highlight">{{ $result->messages->count() }}</strong> messages and <strong class="highlight">{{ $result->password ? 'DOES' : 'does NOT' }}</strong> have a password.</p>
<p>Tosser: <strong class="highlight">{{ $result->software->code }}</strong> (<strong class="highlight">{{ $result->software->name }}</strong>), version <strong class="highlight">{{ $result->software_ver }}</strong>. Capabilities: <strong class="highlight">{{ $result->capability }}</strong>.</p>
@if ($result->messages->count() > 1)
<p><small>You can expand each one</small></p>
@endif
<hr>
<div class="accordion accordion-flush" id="accordion_packetdebug">
<div class="accordion accordion-flush" id="accordion_file_{{ $loop->parent->index }}">
@foreach ($result as $msg)
<div class="row">
<div class="col-12">
<h4 class="accordion-header" id="packetdebug" data-bs-toggle="collapse" data-bs-target="#collapse_msg_{{ $loop->index }}" aria-expanded="false" aria-controls="collapse_addresses">
<h4 class="accordion-header" id="packetdebug" data-bs-toggle="collapse" data-bs-target="#collapse_msg_{{ $loop->parent->index }}_{{ $loop->index }}" aria-expanded="false">
@if($msg->isNetmail()) Netmail @else Echomail <strong>{{ $msg->echoarea }}</strong> @endif : {{ $msg->msgid }}
</h4>
<div id="collapse_msg_{{ $loop->index }}" class="accordion-collapse collapse @if($result->messages->count() == 1 && $loop->first)show @endif" aria-labelledby="packetdebug" data-bs-parent="#accordion_packetdebug">
<div id="collapse_msg_{{ $loop->parent->index }}_{{ $loop->index }}" class="accordion-collapse collapse @if($result->messages->count() == 1 && $loop->parent->first)show @endif" aria-labelledby="packetdebug" data-bs-parent="#accordion_file_{{ $loop->parent->parent->index }}">
<div class="accordion-body">
@if ($msg->errors)
@foreach ($msg->errors->messages()->all() as $error)
@ -142,20 +149,30 @@
</div>
</div>
</div>
</div>
</div>
@endforeach
<h4 class="accordion-header" id="packetdebug" data-bs-toggle="collapse" data-bs-target="#collapse_hex" aria-expanded="false" aria-controls="collapse_hex">
</div>
</div>
<div class="row">
<div class="col-12">
<h4 class="accordion-header" id="packetdebug_{{ $loop->parent->parent->index }}" data-bs-toggle="collapse" data-bs-target="#collapse_hex_{{ $loop->parent->parent->index }}" aria-expanded="false">
Packet Dump
</h4>
<div id="collapse_hex" class="accordion-collapse collapse" aria-labelledby="packetdebug" data-bs-parent="#accordion_packetdebug">
<div id="collapse_hex_{{ $loop->parent->parent->index }}" class="accordion-collapse collapse" aria-labelledby="packetdebug_{{ $loop->parent->parent->index }}" data-bs-parent="#accordion_file_{{ $loop->parent->parent->index }}">
<div class="accordion-body">
<pre>
{{ $hexdump }}
{{ hex_dump($msg) }}
</pre>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endforeach
@endforeach
</div>
@endif
@endsection

View File

@ -2,9 +2,9 @@
namespace Tests\Feature;
use Symfony\Component\HttpFoundation\File\File;
use Tests\TestCase;
use App\Classes\File;
use App\Classes\FTN\Packet;
use App\Models\{Address,Domain,System,Zone};
@ -34,7 +34,8 @@ class PacketTest extends TestCase
// This packet has an incorrect zone in the Origin
$f = new File(__DIR__.'/data/test_nomsgid_origin.pkt');
$pkt = Packet::open($f,NULL,FALSE);
foreach ($f as $packet) {
$pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),NULL,FALSE);
$this->assertEquals(1,$pkt->count());
@ -55,6 +56,7 @@ class PacketTest extends TestCase
$this->assertTrue($messages);
}
}
public function test_nomsgid_noorigin()
{
@ -67,7 +69,8 @@ class PacketTest extends TestCase
// This packet has an incorrect zone in the Origin
$f = new File(__DIR__.'/data/test_nomsgid_noorigin.pkt');
$pkt = Packet::open($f,$this->so,FALSE);
foreach ($f as $packet) {
$pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),$this->so,FALSE);
$this->assertEquals(1,$pkt->count());
@ -84,6 +87,7 @@ class PacketTest extends TestCase
$this->assertTrue($messages);
}
}
public function test_msgid_origin()
{
@ -96,7 +100,8 @@ class PacketTest extends TestCase
// This packet has an incorrect zone in the Origin
$f = new File(__DIR__.'/data/test_msgid_origin.pkt');
$pkt = Packet::open($f,$this->so,FALSE);
foreach ($f as $packet) {
$pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),$this->so,FALSE);
$this->assertEquals(1,$pkt->count());
@ -114,12 +119,14 @@ class PacketTest extends TestCase
$this->assertTrue($messages);
}
}
public function test_packet_parse()
{
// This packet has a SOH<char>SOH sequence
$f = new File(__DIR__.'/data/test_binary_content-2.pkt');
$pkt = Packet::open($f,NULL,FALSE);
foreach ($f as $packet) {
$pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),NULL,FALSE);
$this->assertEquals(1,$pkt->count());
@ -140,10 +147,12 @@ class PacketTest extends TestCase
}
$this->assertTrue($messages);
}
// This packet has SOH in the message content
$f = new File(__DIR__.'/data/test_binary_content.pkt');
$pkt = Packet::open($f,NULL,FALSE);
foreach ($f as $packet) {
$pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),NULL,FALSE);
$this->assertEquals(1,$pkt->count());
@ -164,10 +173,12 @@ class PacketTest extends TestCase
}
$this->assertTrue($messages);
}
// This packet has an incorrect zone in the Origin
$f = new File(__DIR__.'/data/test_msgid_origin.pkt');
$pkt = Packet::open($f,NULL,FALSE);
foreach ($f as $packet) {
$pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),NULL,FALSE);
$this->assertEquals(1,$pkt->count());
@ -190,3 +201,4 @@ class PacketTest extends TestCase
$this->assertTrue($messages);
}
}
}