Added packet debug on web UI

This commit is contained in:
Deon George 2021-06-29 20:43:29 +10:00
parent dc86f7c008
commit 987b4040fb
17 changed files with 1095 additions and 792 deletions

View File

@ -2,8 +2,34 @@
namespace App\Classes;
use Illuminate\Support\Arr;
abstract class FTN
{
public function __get($key)
{
switch ($key) {
case 'fftn':
return sprintf('%d:%d/%d.%d',
$this->fz,
$this->fn,
$this->ff,
$this->fp,
);
case 'tftn':
return sprintf('%d:%d/%d.%d',
$this->tz,
$this->tn,
$this->tf,
$this->tp,
);
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Determine if a line is a kludge line.
*
@ -20,12 +46,18 @@ abstract class FTN
/**
* This function creates our unpack header
*
* @param array $pack
* @return string
*/
protected function unpackheader(array $pack)
protected function unpackheader(array $pack): string
{
return join('/',array_values(collect($pack)
return join('/',
collect($pack)
->sortBy(function($k,$v) {return $k[0];})
->transform(function($k,$v) {return $k[1].$v;})->toArray()));
->transform(function($k,$v) {return $k[1].$v;})
->values()
->toArray()
);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Classes\FTN;
use Exception;
class InvalidPacketException extends Exception
{
//
}

475
app/Classes/FTN/Message.php Normal file
View File

@ -0,0 +1,475 @@
<?php
namespace App\Classes\FTN;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Validator as ValidatorResult;
use App\Classes\FTN as FTNBase;
use App\Models\Address;
use App\Rules\TwoByteInteger;
use App\Traits\GetNode;
/**
* Class Message
* NOTE: FTN Echomail Messages are ZONE agnostic.
*
* @package App\Classes
*/
class Message extends FTNBase
{
//use GetNode;
// Single value kludge items
private array $_kludge = [
'chrs' => 'CHRS: ',
'charset' => 'CHARSET: ',
'codepage' => 'CODEPAGE: ',
'msgid' => 'MSGID: ',
'pid' => 'PID: ',
'replyid' => 'REPLY: ',
'tid' => 'TID: ',
'tzutc' => 'TZUTC: ',
];
// Flags for messages
public const FLAG_PRIVATE = 1<<0;
public const FLAG_CRASH = 1<<1;
public const FLAG_RECD = 1<<2;
public const FLAG_SENT = 1<<3;
public const FLAG_FILEATTACH = 1<<4;
public const FLAG_INTRANSIT = 1<<5;
public const FLAG_ORPHAN = 1<<6;
public const FLAG_KILLSENT = 1<<7;
public const FLAG_LOCAL = 1<<8;
public const FLAG_HOLD = 1<<9;
public const FLAG_UNUSED_10 = 1<<10;
public const FLAG_FREQ = 1<<11;
public const FLAG_RETRECEIPT = 1<<12;
public const FLAG_ISRETRECEIPT = 1<<13;
public const FLAG_AUDITREQ = 1<<14;
public const FLAG_FILEUPDATEREQ = 1<<15;
public const FLAG_ECHOMAIL = 1<<16;
// FTS-0001.016 Message header 32 bytes node, net, flags, cost, date
private const HEADER_LEN = 0x20; // Length of message header
private const header = [ // Struct of message header
'onode' => [0x00,'v',2], // Originating Node
'dnode' => [0x02,'v',2], // Destination Node
'onet' => [0x04,'v',2], // Originating Net
'dnet' => [0x06,'v',2], // Destination Net
'flags' => [0x08,'v',2], // Message Flags
'cost' => [0x0a,'v',2], // Send Cost
'date' => [0x0c,'A20',20] // Message Date FTS-0001.016 Date: upto 20 chars null terminated
];
private const USER_FROM_LEN = 36; // FTS-0001.016 From Name: upto 36 chars null terminated
private const USER_TO_LEN = 36; // FTS-0001.016 To Name: upto 36 chars null terminated
private const SUBJECT_LEN = 72; // FTS-0001.016 Subject: upto 72 chars null terminated
private const AREATAG_LEN = 35; //
private ?ValidatorResult $errors = NULL; // Packet validation
private array $header; // Message Header
private Collection $kludge; // Hold kludge items
private string $user_from; // User message is From
private string $user_to; // User message is To
private string $subject; // Message subject
private string $message; // The actual message content
private string $origin; // FTS-0004.001
private ?string $echoarea = NULL; // FTS-0004.001
private array $zone; // Zone the message belongs to. (src/dst - for netmail)
private array $netmail; // Netmail details
private Collection $path; // FTS-0004.001 The message PATH lines
private Collection $seenby; // FTS-0004.001 The message SEEN-BY lines
private Collection $via; // The path the message has gone using Via lines (Netmail)
private Collection $_other; // Temporarily hold attributes we dont process yet.
private Collection $unknown; // Temporarily hold attributes we have no logic for.
public function __construct(string $msg)
{
$this->kludge = collect();
$this->path = collect();
$this->seenby = collect();
$this->via = collect();
$this->_other = collect();
$this->unknown = collect();
$this->zone = [];
$this->header = unpack($this->unpackheader(self::header),substr($msg,0,self::HEADER_LEN));
$ptr = 0;
// To User
$this->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
$ptr += strlen($this->user_to)+1;
// From User
$this->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
$ptr += strlen($this->user_from)+1;
// Subject
$this->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
$ptr += strlen($this->subject)+1;
// Check if this is an Echomail
if (! strncmp(substr($msg,self::HEADER_LEN+$ptr),'AREA:',5)) {
$this->echoarea = substr($msg,self::HEADER_LEN+$ptr+5,strpos($msg,"\r",self::HEADER_LEN+$ptr+5)-(self::HEADER_LEN+$ptr+5));
$ptr += strlen($this->echoarea)+5+1;
}
$this->parseMessage(substr($msg,self::HEADER_LEN+$ptr));
if (($x=$this->validate()->getMessageBag())->count())
Log::debug('Message fails validation',['result'=>$x]);
}
public function __get($key)
{
switch ($key) {
// From Addresses
case 'fz': return Arr::get($this->zone,'src',0);
case 'fn': return Arr::get($this->header,'onet');
case 'ff': return Arr::get($this->header,'onode');
case 'fp': return 0; // @todo
// To Addresses
// Echomail doesnt have a zone, so we'll use the source zone
case 'tz': return Arr::get($this->zone,$this->echoarea ? 'src' : 'dst',0);
case 'tn': return Arr::get($this->header,'dnet');
case 'tf': return Arr::get($this->header,'dnode');
case 'tp': return 0; // @todo
case 'fftn':
case 'tftn':
return parent::__get($key);
case 'date':
return sprintf('%s (%s)',Arr::get($this->header,$key),$this->kludge->get('tzutc'));
case 'flags':
case 'cost': return Arr::get($this->header,$key);
case 'msgid': return $this->kludge->get('msgid');
case 'message':
case 'subject':
case 'user_to':
case 'user_from':
case 'kludge':
case 'path':
case 'seenby':
case 'errors':
case 'echoarea':
return $this->{$key};
/*
case 'tearline':
return '--- FTNHub';
*/
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Export an FTN message, ready for sending.
*
* @return string
* @todo To rework
*/
public function __toString(): string
{
// if (f->net == 65535) { /* Point packet - Get Net from auxNet */
$return = '';
$return .= pack(join('',collect(self::header)->pluck(1)->toArray()),
$this->ff,
$this->tf,
$this->fn,
$this->tn,
$this->flags,
$this->cost
);
// @todo use pack for this.
$return .= $this->date->format('d M y H:i:s')."\00";
$return .= $this->to."\00";
$return .= $this->from."\00";
$return .= $this->subject."\00";
if ($this->type == 'echomail')
$return .= "AREA:".$this->echoarea."\r";
// Add some kludges
$return .= "\01MSGID ".$this->_fqfa." 1"."\r";
foreach ($this->_kludge as $k=>$v) {
if ($x=$this->kludge->get($k))
$return .= chr(1).$v.$x."\r";
}
$return .= $this->message."\r";
$return .= $this->tearline."\r";
$return .= $this->origin."\r";
switch ($this->type)
{
case 'echomail':
break;
case 'netmail':
foreach ($this->via as $k=>$v)
$return .= "\01Via: ".$v."\r";
// @todo Set product name/version as var
$return .= sprintf('%sVia: %s @%s.UTC %s %i.%i',
chr(1),
'10:0/0',
now('UTC')->format('Ymd.His'),
'FTNHub',
1,1)."\r";
break;
}
$return .= "\00";
return $return;
}
/**
* Return an array of flag descriptions
*
* @return array
*
* http://ftsc.org/docs/fsc-0001.000
* AttributeWord bit meaning
--- --------------------
0 + Private
1 + s Crash
2 Recd
3 Sent
4 + FileAttached
5 InTransit
6 Orphan
7 KillSent
8 Local
9 s HoldForPickup
10 + unused
11 s FileRequest
12 + s ReturnReceiptRequest
13 + s IsReturnReceipt
14 + s AuditRequest
15 s FileUpdateReq
s - this bit is supported by SEAdog only
+ - this bit is not zeroed before packeting
*/
/*
public function flags(int $flags): array
{
return [
'private' => $this->isFlagSet($flags,self::FLAG_PRIVATE),
'crash' => $this->isFlagSet($flags,self::FLAG_CRASH),
'recd' => $this->isFlagSet($flags,self::FLAG_RECD),
'sent' => $this->isFlagSet($flags,self::FLAG_SENT),
'killsent' => $this->isFlagSet($flags,self::FLAG_KILLSENT),
'local' => $this->isFlagSet($flags,self::FLAG_LOCAL),
];
}
private function isFlagSet($value,$flag): bool
{
return (($value & $flag) == $flag);
}
*/
/**
* If this message doesnt have an AREATAG, then its a netmail.
*
* @return bool
*/
public function isNetmail(): bool
{
return ! $this->echoarea;
}
/**
* Extract information out of the message text.
*
* @param string $message
* @throws InvalidPacketException
*/
public function parseMessage(string $message): void
{
// Remove DOS \n\r
$message = preg_replace("/\n\r/","\r",$message);
// Split out the <SOH> lines
$result = collect(explode("\01",$message))->filter();
$this->message = '';
foreach ($result as $v) {
// Search for \r - if that is the end of the line, then its a kludge
$x = strpos($v,"\r");
$t = '';
// If there are more characters, then put the kludge back into the result, so that we process it.
if ($x != strlen($v)-1) {
/**
* Anything after the origin line is also kludge data.
*/
if ($y = strpos($v,"\r * Origin: ")) {
$this->message .= substr($v,$x+1,$y-$x-1);
$this->parseOrigin(substr($v,$y));
// If this is netmail, the FQFA will have been set by the INTL line, we can skip the rest of this
$matches = [];
// Capture the fully qualified 4D name from the Origin Line - it tells us the ZONE.
preg_match('/^.*\((.*)\)$/',$this->origin,$matches);
// Double check we have an address in the origin line
if (! Arr::get($matches,1))
throw new InvalidPacketException(sprintf('No address in Origin?',$matches));
// Double check, our src and origin match
$ftn = Address::parseFTN($matches[1]);
// We'll double check our FTN
if (($ftn['n'] !== $this->fn) || ($ftn['f'] !== $this->ff)) {
Log::error(sprintf('FTN [%s] doesnt match message header',$matches[1]),['ftn'=>$ftn]);
}
$this->zone['src'] = $ftn['z'];
// The message is the rest?
} elseif (strlen($v) > $x+1) {
$this->message .= substr($v,$x+1);
}
$v = substr($v,0,$x+1);
}
foreach ($this->_kludge as $a => $b) {
if ($t = $this->kludge($b,$v)) {
$this->kludge->put($a,$t);
break;
}
}
// There is more text.
if ($t)
continue;
// From point: <SOH>"FMPT <point number><CR>
if ($t = $this->kludge('FMPT ',$v))
$this->_other->push($t);
/*
* The INTL control paragraph shall be used to give information about
* the zone numbers of the original sender and the ultimate addressee
* of a message.
*
* <SOH>"INTL "<destination address>" "<origin address><CR>
*/
elseif ($t = $this->kludge('INTL ',$v)) {
$this->netmail['intl'] = $t;
list($this->netmail['dst'],$this->netmail['src']) = explode(' ',$t);
}
elseif ($t = $this->kludge('PATH: ',$v))
$this->path->push($t);
// To Point: <SOH>TOPT <point number><CR>
elseif ($t = $this->kludge('TOPT ',$v))
$this->_other->push($t);
// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
elseif ($t = $this->kludge('Via ',$v))
$this->via->push($t);
// We got a kludge line we dont know about
else
$this->unknown->push(chop($v,"\r"));
}
}
/**
* Process the data after the ORIGIN
* There may be kludge lines after the origin - notably SEEN-BY
*
* @param string $message
*/
private function parseOrigin(string $message)
{
// Split out each line
$result = collect(explode("\r",$message))->filter();
foreach ($result as $v) {
foreach ($this->_kludge as $a => $b) {
if ($t = $this->kludge($b,$v)) {
$this->kludge->put($a,$t);
break;
}
}
if ($t = $this->kludge('SEEN-BY: ', $v))
$this->seenby->push($t);
elseif ($t = $this->kludge('PATH: ', $v))
$this->path->push($t);
elseif ($t = $this->kludge(' \* Origin: ',$v))
$this->origin = $t;
// We got unknown Kludge lines in the origin
else
$this->unknown->push($v);
}
}
/**
* Validate details about this message
*
* @return \Illuminate\Contracts\Validation\Validator
*/
private function validate(): ValidatorResult
{
// Check lengths
$validator = Validator::make([
'user_from' => $this->user_from,
'user_to' => $this->user_to,
'subject' => $this->subject,
'onode' => $this->fn,
'dnode' => $this->ff,
'onet' => $this->tn,
'dnet' => $this->tf,
'flags' => $this->flags,
'cost' => $this->cost,
'echoarea' => $this->echoarea,
],[
'user_from' => 'required|min:1|max:'.self::USER_FROM_LEN,
'user_to' => 'required|min:1|max:'.self::USER_TO_LEN,
'subject' => 'required|max:'.self::SUBJECT_LEN,
'onode' => ['required',new TwoByteInteger],
'dnode' => ['required',new TwoByteInteger],
'onet' => ['required',new TwoByteInteger],
'dnet' => ['required',new TwoByteInteger],
'flags' => 'required|numeric',
'cost' => 'required|numeric',
'echoarea' => 'nullable|max:'.self::AREATAG_LEN,
]);
if ($validator->fails())
$this->errors = $validator;
return $validator;
}
}

308
app/Classes/FTN/Packet.php Normal file
View File

@ -0,0 +1,308 @@
<?php
namespace App\Classes\FTN;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\File;
use App\Classes\FTN as FTNBase;
use App\Models\Software;
use App\Traits\GetNode;
class Packet extends FTNBase
{
//use GetNode;
private const LOGKEY = 'PKT';
private const HEADER_LEN = 0x3a;
private const VERSION_OFFSET = 0x12;
private const BLOCKSIZE = 1024;
private const PACKED_MSG_HEADER_LEN = 0x22;
public File $file; // Packet filename
public Collection $messages; // Messages in the Packet
private array $header; // Packet Header
// V2 Packet Header (2/2e/2+)
private const v2header = [
'onode' => [0x00,'v',2], // Originating Node
'dnode' => [0x02,'v',2], // Destination Node
'y' => [0x04,'v',2], // Year
'm' => [0x06,'v',2], // Month
'd' => [0x08,'v',2], // Day
'H' => [0x0a,'v',2], // Hour
'M' => [0x0c,'v',2], // Minute
'S' => [0x0e,'v',2], // Second
'baud' => [0x10,'v',2], // Baud
'pktver' => [0x12,'v',2], // Packet Version
'onet' => [0x14,'v',2], // Originating Net (0xffff when origPoint !=0 2+)
'dnet' => [0x16,'v',2], // Destination Net
'prodcode-lo' => [0x18,'C',1],
'prodrev-maj' => [0x19,'C',1], // Product Version Major (serialNum 2)
'password' => [0x1a,'Z8',8], // Packet Password
'qozone' => [0x22,'v',2],
'qdzone' => [0x24,'v',2],
'filler' => [0x26,'v',2], // Reserved (auxnet 2+ - contains Orignet if Origin is a point) fsc-0048.001
'capvalid' => [0x28,'v',2], // fsc-0039.004 (Not used 2) (copy of 0x2c)
'prodcode-hi' => [0x2a,'C',1], // (Not used 2)
'prodrev-min' => [0x2b,'C',1], // (Not used 2)
'capword' => [0x2c,'v',1], // fsc-0039.001 (Not used 2)
'ozone' => [0x2e,'v',2], // Originating Zone (Not used 2)
'dzone' => [0x30,'v',2], // Destination Zone (Not used 2)
'opoint' => [0x32,'v',2], // Originating Point (Not used 2)
'dpoint' => [0x34,'v',2], // Destination Point (Not used 2)
'proddata' => [0x36,'A4',4], // ProdData (Not used 2) // FSC-39/FSC-48
];
public function __construct(File $file)
{
$this->messages = collect();
if ($file) {
$this->file = $file;
$this->open($file);
}
}
/**
* @throws \Exception
*/
public function __get($key)
{
switch ($key) {
// From Addresses
case 'fz': return Arr::get($this->header,'ozone');
case 'fn': return Arr::get($this->header,'onet');
case 'ff': return Arr::get($this->header,'onode');
case 'fp': return Arr::get($this->header,'opoint');
case 'fd': return Arr::get($this->header,'odomain');
// To Addresses
case 'tz': return Arr::get($this->header,'dzone');
case 'tn': return Arr::get($this->header,'dnet');
case 'tf': return Arr::get($this->header,'dnode');
case 'tp': return Arr::get($this->header,'dpoint');
case 'td': return Arr::get($this->header,'ddomain');
case 'date':
return Carbon::create(
Arr::get($this->header,'y'),
Arr::get($this->header,'m')+1,
Arr::get($this->header,'d'),
Arr::get($this->header,'H'),
Arr::get($this->header,'M'),
Arr::get($this->header,'S')
);
case 'capability':
return Arr::get($this->header,'capword') == Arr::get($this->header,'capword') ? sprintf('%016b',Arr::get($this->header,'capword')) : 'FTS-1';
case 'password':
return Arr::get($this->header,$key);
case 'fftn':
case 'tftn':
return parent::__get($key);
case 'software':
$code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo');
Software::unguard();
$o = Software::singleOrNew(['code'=>$code,'type'=>Software::SOFTWARE_TOSSER]);
Software::reguard();
return $o;
case 'software_ver':
return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min'));
// Packet Type
case 'type':
if ((Arr::get($this->header,'onet') == 0xffff) && (Arr::get($this->header,'opoint') != 0) && Arr::get($this->header,'filler'))
return '2+';
elseif (Arr::get($this->header,'prodrev-maj') && ! Arr::get($this->header,'capword'))
return '2';
else
return '2e';
default:
throw new \Exception('Unknown key: '.$key);
}
}
// @note - messages in this object have the same next destination
// @todo To rework
/*
public function __toString(): string
{
// @todo - is this appropriate to set here
$this->date = now();
$this->pktsrc = (string)$this->get_node(ftn_address_split('10:1/5.0'),TRUE);
// @todo
if ($this->messages->first()->type == 'echomail')
$this->pktdst = (string)$this->messages->first()->fqfa->uplink;
else
$this->pktdst = (string)$this->messages->first()->fqda->uplink;
$this->software['prodcode-lo'] = 0x00;
$this->software['prodcode-hi'] = 0xde;
$this->software['rev-maj'] = 0x00;
$this->software['rev-min'] = 0x01;
// Type 2+ Packet
$this->cap['valid'] = 0x0100;
$this->cap['word'] = 0x0001;
$this->pktver = 0x0002;
$return = $this->createHeader();
foreach ($this->messages as $o)
$return .= "\02\00".(string)$o;
$return .= "\00\00";
return $return;
}
*/
/**
* Create our message packet header
* @todo To rework
*/
/*
private function createHeader(): string
{
try {
$a = pack(join('',collect($this->pack1)->pluck(1)->toArray()),
$this->ff,
$this->tf,
$this->date->year,
$this->date->month,
$this->date->day,
$this->date->hour,
$this->date->minute,
$this->date->second,
$this->baud,
$this->pktver,
$this->fn, // @todo if point, this needs to be 0xff
$this->tn,
$this->software['prodcode-lo'], // @todo change to this software
$this->software['rev-maj'] // @todo change to this software
);
$b = pack(join('',collect($this->pack2)->pluck(1)->toArray()),
0x0000, // @note: Type 2 packet this is $this->sz,
0x0000, // @note: Type 2 packet this is $this->dz,
0x0000, // Filler $this->>sn if message to point.
$this->cap['valid'], // @todo to check
$this->software['prodcode-hi'], // @todo change to this software
$this->software['rev-min'], // @todo change to this software
$this->cap['word'], // @todo to check
$this->fz,
$this->tz,
$this->fp, // @note: point address, type 2+ packets
$this->tp // @note: point address, type 2+ packets
);
return $a.pack('a8',strtoupper($this->password)).$b."mbse"; // @todo change to this software
} catch (\Exception $e) {
return $e->getMessage();
}
}
public function addMessage(FTNMessage $o)
{
// @todo Check that this message is for the same uplink.
$this->messages->push($o);
}
*/
/**
* Open a packet file
*
* @param string $file
* @throws InvalidPacketException
*/
private function open(string $file)
{
Log::debug(sprintf('%s:Opening Packet [%s]',self::LOGKEY,$file));
$f = fopen($file,'r');
$fstat = fstat($f);
// PKT Header
$header = fread($f,self::HEADER_LEN);
Log::debug(sprintf("%s:\n%s",self::LOGKEY,hex_dump($header)));
// Could not read header
if (strlen($header) != self::HEADER_LEN)
throw new InvalidPacketException(sprintf('Length of header [%d] too short'.strlen($header)));
// Not a type 2 packet
$version = Arr::get(unpack('vv',substr($header,self::VERSION_OFFSET)),'v');
if ($version != 2)
throw new InvalidPacketException('Not a type 2 packet: '.$version);
$this->header = unpack($this->unpackheader(self::v2header),$header);
$x = fread($f,2);
// End of Packet?
if (strlen($x) == 2 and $x == "\00\00")
return;
// Messages start with 02H 00H
if (strlen($x) == 2 AND $x != "\02\00")
throw new InvalidPacketException('Not a valid packet: '.bin2hex($x));
// No message attached
else if (! strlen($x))
throw new InvalidPacketException('No message in packet: '.bin2hex($x));
$buf_ptr = 0;
$message = '';
$readbuf = '';
while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) {
// A message header is atleast 0x22 chars long
if (strlen($readbuf) < self::PACKED_MSG_HEADER_LEN) {
$message .= $readbuf;
$buf_ptr = 0;
continue;
} elseif (strlen($message) < self::PACKED_MSG_HEADER_LEN) {
$addchars = self::PACKED_MSG_HEADER_LEN-strlen($message);
$message .= substr($readbuf,$buf_ptr,$addchars);
$buf_ptr += $addchars;
}
// If we didnt find a packet end, perhaps there are no more
if (($end=strpos($readbuf,"\x00\x02\x00",$buf_ptr)) === FALSE)
$end = strpos($readbuf,"\x00\x00\x00",$buf_ptr);
// See if we have found the end of the packet, if not read more.
if ($end === FALSE && (ftell($f) < $fstat['size'])) {
$message .= substr($readbuf,$buf_ptr);
$buf_ptr = 0;
continue;
} else {
$message .= substr($readbuf,$buf_ptr,$end-$buf_ptr);
$buf_ptr += $end-$buf_ptr+3;
if ($buf_ptr >= strlen($readbuf))
$buf_ptr = 0;
}
// Look for the next message
$this->messages->push(new Message($message));
$message = '';
}
}
}

View File

@ -1,453 +0,0 @@
<?php
namespace App\Classes;
use Illuminate\Support\Arr;
use App\Exceptions\InvalidFidoPacketException;
use App\Traits\GetNode;
/**
* Class FTNMessage
* NOTE: FTN Echomail Messages are ZONE agnostic.
*
* @package App\Classes
*/
class FTNMessage extends FTN
{
use GetNode;
private $src = NULL; // SRC N/F from packet
private $dst = NULL; // DST N/F from packet
private $flags = NULL; // Flags from packet
private $cost = 0; // Cost from packet
// @todo need to validate these string lengths when creating packet.
private $from = NULL; // FTS-0001.016 From Name: upto 36 chars null terminated
private $to = NULL; // FTS-0001.016 To Name: upto 36 chars null terminated
private $subject = NULL; // FTS-0001.016 Subject: upto 72 chars null terminated
private $date = NULL; // FTS-0001.016 Date: upto 20 chars null terminated
private $message = NULL; // The actual message content
private $echoarea = NULL; // FTS-0004.001
private $intl = NULL;
private $msgid = NULL;
private $reply = NULL; // Message thread reply source
private $origin = NULL; // FTS-0004.001
private $kludge = []; // Hold kludge items
private $path = []; // FTS-0004.001
private $seenby = []; // FTS-0004.001
private $via = [];
private $_other = [];
private $unknown = [];
// We auto create these values - they are used to create packets.
private $_fqfa = NULL; // Fully qualified fidonet source where packet originated
private $_fqda = NULL; // Fully qualified fidonet destination address (Netmail)
// Single value kludge items
private $_kludge = [
'chrs' => 'CHRS: ',
'charset' => 'CHARSET: ',
'codepage' => 'CODEPAGE: ',
'pid' => 'PID: ',
'tid' => 'TID: ',
];
// Flags for messages
const FLAG_PRIVATE = 0b1;
const FLAG_CRASH = 0b10;
const FLAG_RECD = 0b100;
const FLAG_SENT = 0b1000;
const FLAG_FILEATTACH = 0b10000;
const FLAG_INTRANSIT = 0b100000;
const FLAG_ORPHAN = 0b1000000;
const FLAG_KILLSENT = 0b10000000;
const FLAG_LOCAL = 0b100000000;
const FLAG_HOLD = 0b1000000000;
const FLAG_UNUSED_10 = 0b10000000000;
const FLAG_FREQ = 0b100000000000;
const FLAG_RETRECEIPT = 0b1000000000000;
const FLAG_ISRETRECEIPT = 0b10000000000000;
const FLAG_AUDITREQ = 0b100000000000000;
const FLAG_FILEUPDATEREQ = 0b1000000000000000;
// FTS-0001.016 Message header 12 bytes
// node, net, flags, cost
private $struct = [
'onode'=>[0x00,'v',2],
'dnode'=>[0x02,'v',2],
'onet'=>[0x04,'v',2],
'dnet'=>[0x06,'v',2],
'flags'=>[0x08,'v',2],
'cost'=>[0x0a,'v',2],
];
public function __construct(string $header=NULL)
{
// Initialise vars
$this->kludge = collect(); // The message kludge lines
$this->path = collect(); // The message PATH lines
$this->seenby = collect(); // The message SEEN-BY lines
$this->via = collect(); // The path the message has gone using Via lines
$this->_other = collect(); // Temporarily hold attributes we dont process yet.
$this->unknown = collect(); // Temporarily hold attributes we have no logic for.
if ($header)
$this->parseheader($header);
}
public function __get($k)
{
switch ($k)
{
case 'fz': return ftn_address_split($this->_fqfa,'z');
case 'fn': return ftn_address_split($this->_fqfa,'n');
case 'ff': return ftn_address_split($this->_fqfa,'f');
case 'fp': return ftn_address_split($this->_fqfa,'p');
case 'fqfa': return $this->_fqfa;
case 'fqda': return $this->_fqda;
// Echomails dont have a fully qualified from address
case 'tz': return ftn_address_split($this->_fqda,'z');
case 'tn': return ftn_address_split($this->_fqda,'n');
case 'tf': return ftn_address_split($this->_fqda,'f');
case 'tp': return ftn_address_split($this->_fqda,'p');
case 'tearline':
return '--- FTNHub';
case 'type':
if ($this->echoarea)
return 'echomail';
if ($this->intl)
return 'netmail';
return NULL;
default:
return isset($this->{$k}) ? $this->{$k} : NULL;
}
}
public function __set($k,$v)
{
switch ($k)
{
case 'fqfa':
case 'fqda':
$this->{'_'.$k} = $this->get_node(ftn_address_split($v),TRUE);
if ($this->_fqfa AND $this->_fqda)
$this->intl = sprintf('%s %s',$this->_fqda,$this->_fqfa);
case 'origin':
if (! $this->_fqfa)
throw new \Exception('Must set from address before origin');
$this->origin = sprintf(' * Origin: %s (%s)',$v,$this->_fqfa);
break;
default:
$this->{$k} = $v;
}
}
/**
* Export an FTN message, ready for sending.
*
* @return string
*/
public function __toString(): string
{
// if (f->net == 65535) { /* Point packet - Get Net from auxNet */
$return = '';
$return .= pack(join('',collect($this->struct)->pluck(1)->toArray()),
$this->ff,
$this->tf,
$this->fn,
$this->tn,
$this->flags,
$this->cost
);
// @todo use pack for this.
$return .= $this->date->format('d M y H:i:s')."\00";
$return .= $this->to."\00";
$return .= $this->from."\00";
$return .= $this->subject."\00";
if ($this->type == 'echomail')
$return .= "AREA:".$this->echoarea."\r";
// Add some kludges
$return .= "\01MSGID ".$this->_fqfa." 1"."\r";
foreach ($this->_kludge as $k=>$v)
{
if ($x=$this->kludge->get($k))
$return .= chr(1).$v.$x."\r";
}
$return .= $this->message."\r";
$return .= $this->tearline."\r";
$return .= $this->origin."\r";
switch ($this->type)
{
case 'echomail':
break;
case 'netmail':
foreach ($this->via as $k=>$v)
$return .= "\01Via: ".$v."\r";
// @todo Set product name/version as var
$return .= sprintf('%sVia: %s @%s.UTC %s %i.%i',
chr(1),
'10:0/0',
now('UTC')->format('Ymd.His'),
'FTNHub',
1,1)."\r";
break;
}
$return .= "\00";
return $return;
}
/**
* Return an array of flag descriptions
*
* @return array
*
* http://ftsc.org/docs/fsc-0001.000
* AttributeWord bit meaning
--- --------------------
0 + Private
1 + s Crash
2 Recd
3 Sent
4 + FileAttached
5 InTransit
6 Orphan
7 KillSent
8 Local
9 s HoldForPickup
10 + unused
11 s FileRequest
12 + s ReturnReceiptRequest
13 + s IsReturnReceipt
14 + s AuditRequest
15 s FileUpdateReq
s - this bit is supported by SEAdog only
+ - this bit is not zeroed before packeting
*/
public function flags(int $flags): array
{
return [
'private'=>$this->isFlagSet($flags,self::FLAG_PRIVATE),
'crash'=>$this->isFlagSet($flags,self::FLAG_CRASH),
'recd'=>$this->isFlagSet($flags,self::FLAG_RECD),
'sent'=>$this->isFlagSet($flags,self::FLAG_SENT),
'killsent'=>$this->isFlagSet($flags,self::FLAG_KILLSENT),
'local'=>$this->isFlagSet($flags,self::FLAG_LOCAL),
];
}
private function isFlagSet($value,$flag)
{
return (($value & $flag) == $flag);
}
/**
* Parse the head of an FTN message
*
* @param string $header
*/
public function parseheader(string $header)
{
$result = unpack($this->unpackheader($this->struct),$header);
// For Echomail this is the packet src.
$this->psn = Arr::get($result,'onet');
$this->psf = Arr::get($result,'onode');
$this->src = sprintf('%s/%s',
$this->psn,
$this->psf
);
// For Echomail this is the packet dst.
$this->pdn = Arr::get($result,'dnet');
$this->pdf = Arr::get($result,'dnode');
$this->dst = sprintf('%s/%s',
$this->pdn,
$this->pdf
);
$this->flags = Arr::get($result,'flags');
$this->cost = Arr::get($result,'cost');
}
public function parsemessage(string $message)
{
// Remove DOS \n\r
$message = preg_replace("/\n\r/","\r",$message);
// Split out the <SOH> lines
$result = collect(explode("\01",$message))->filter();
foreach ($result as $k => $v)
{
// Search for \r - if that is the end of the line, then its a kludge
$x = strpos($v,"\r");
// If there are more characters, then put the kludge back into the result, so that we process it.
if ($x != strlen($v)-1)
{
/**
* Anything after the origin line is also kludge data.
*/
if ($y = strpos($v,"\r * Origin: "))
{
$this->message .= substr($v,$x+1,$y-$x-1);
$this->parseorigin(substr($v,$y));
// If this is netmail, the FQFA will have been set by the INTL line, we can skip the rest of this
$matches = [];
// Capture the fully qualified 4D name from the Origin Line - it tells us the ZONE.
preg_match('/^.*\((.*)\)$/',$this->origin,$matches);
// Double check we have an address in the origin line
if (! Arr::get($matches,1))
throw new InvalidFidoPacketException(sprintf('No address in Origin?',$matches));
// Double check, our src and origin match
if (! preg_match('#^[0-9]+:'.$this->src.'#',$matches[1]))
throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->_fqfa,$matches[1]));
// If this is netmail, a double check our FQFA matches
if ($this->type == 'netmail') {
if ($this->_fqfa != $matches[1])
throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->_fqfa,$matches[1]));
// For other types, this is our only way of getting a FQFA
} else {
$this->_fqfa = $matches[1];
// Our FQDA is not available, we'll assume its the same zone as our FQFA
$this->_fqda = sprintf('%d:%s',ftn_address_split($this->_fqfa,'z'),$this->dst);
}
}
$v = substr($v,0,$x+1);
}
foreach ($this->_kludge as $a => $b) {
if ($t = $this->kludge($b,$v)) {
$this->kludge->put($a,$t);
break;
}
}
if ($t)
continue;
if ($t = $this->kludge('AREA:',$v))
$this->echoarea = $t;
// From point: <SOH>"FMPT <point number><CR>
elseif ($t = $this->kludge('FMPT ',$v))
$this->_other->push($t);
/*
* The INTL control paragraph shall be used to give information about
* the zone numbers of the original sender and the ultimate addressee
* of a message.
*
* <SOH>"INTL "<destination address>" "<origin address><CR>
*/
elseif ($t = $this->kludge('INTL ',$v))
{
$this->intl = $t;
list($this->_fqda,$this->_fqfa) = explode(' ',$t);
}
elseif ($t = $this->kludge('MSGID: ',$v))
$this->msgid = $t;
elseif ($t = $this->kludge('PATH: ',$v))
$this->path->push($t);
elseif ($t = $this->kludge('REPLY: ',$v))
$this->reply = $t;
// To Point: <SOH>TOPT <point number><CR>
elseif ($t = $this->kludge('TOPT ',$v))
$this->_other->push($t);
// Time Zone of the sender.
elseif ($t = $this->kludge('TZUTC: ',$v))
$this->tzutc= $t;
// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
elseif ($t = $this->kludge('Via ',$v))
$this->via->push($t);
// We got a kludge line we dont know about
else {
$this->unknown->push(chop($v,"\r"));
//dd(['v'=>$v,'t'=>$t]);
}
}
}
/**
* Process the data after the ORIGIN
* There may be kludge lines after the origin - notably SEEN-BY
*
* @param string $message
*/
private function parseorigin(string $message)
{
// Split out each line
$result = collect(explode("\r",$message))->filter();
foreach ($result as $k => $v) {
foreach ($this->_kludge as $a => $b) {
if ($t = $this->kludge($b,$v)) {
$this->kludge->put($a,$t);
break;
}
}
if ($t = $this->kludge('SEEN-BY: ', $v))
$this->seenby->push($t);
elseif ($t = $this->kludge('PATH: ', $v))
$this->path->push($t);
elseif ($t = $this->kludge(' \* Origin: ',$v))
$this->origin = $t;
// We got unknown Kludge lines in the origin
else {
$this->unknown->push($v);
//dd(['v'=>$v,'t'=>$t,'message'=>$message]);
}
}
}
}

View File

@ -1,302 +0,0 @@
<?php
namespace App\Classes;
use Carbon\Carbon;
use App\Exceptions\InvalidFidoPacketException;
use App\Traits\GetNode;
class FTNPacket extends FTN
{
use GetNode;
public $pktsrc = NULL;
public $pktdst = NULL;
private $pktver = NULL;
public $date = NULL;
private $baud = NULL;
private $software = [];
private $cap = [];
private $proddata = NULL;
private $password = NULL;
private $sz = NULL;
private $dz = NULL;
private $sn = NULL;
private $dn = NULL;
private $sf = NULL;
private $df = NULL;
private $sp = NULL;
private $dp = NULL;
public $filename = NULL;
public $messages = NULL;
// First part of header
private $pack1 = [
'onode'=>[0x00,'v',2],
'dnode'=>[0x02,'v',2],
'y'=>[0x04,'v',2],
'm'=>[0x06,'v',2],
'd'=>[0x08,'v',2],
'H'=>[0x0a,'v',2],
'M'=>[0x0c,'v',2],
'S'=>[0x0e,'v',2],
'baud'=>[0x10,'v',2],
'pktver'=>[0x12,'v',2],
'onet'=>[0x14,'v',2],
'dnet'=>[0x16,'v',2],
'prodcode-lo'=>[0x18,'C',1],
'prodrev-maj'=>[0x19,'C',1],
];
// Second part of header
private $pack2 = [
'qozone'=>[0x22,'v',2],
'qdzone'=>[0x24,'v',2],
'filler'=>[0x26,'v',2],
'capvalid'=>[0x28,'v',2],
'prodcode-hi'=>[0x2a,'C',1],
'prodrev-min'=>[0x2b,'C',1],
'capword'=>[0x2c,'v',1],
'ozone'=>[0x2e,'v',2],
'dzone'=>[0x30,'v',2],
'opoint'=>[0x32,'v',2],
'dpoint'=>[0x34,'v',2],
];
public function __construct(string $file=NULL)
{
$this->messages = collect();
if ($file) {
$this->filename = $file;
return $this->OpenFile($file);
}
}
public function __get($k)
{
switch ($k)
{
case 'fz': return ftn_address_split($this->pktsrc,'z');
case 'fn': return ftn_address_split($this->pktsrc,'n');
case 'ff': return ftn_address_split($this->pktsrc,'f');
case 'fp': return ftn_address_split($this->pktsrc,'p');
case 'tz': return ftn_address_split($this->pktdst,'z');
case 'tn': return ftn_address_split($this->pktdst,'n');
case 'tf': return ftn_address_split($this->pktdst,'f');
case 'tp': return ftn_address_split($this->pktdst,'p');
default:
return isset($this->{$k}) ? $this->{$k} : NULL;
}
}
// @note - messages in this object have the same next destination
public function __toString(): string
{
// @todo - is this appropriate to set here
$this->date = now();
$this->pktsrc = (string)$this->get_node(ftn_address_split('10:1/5.0'),TRUE);
// @todo
if ($this->messages->first()->type == 'echomail')
$this->pktdst = (string)$this->messages->first()->fqfa->uplink;
else
$this->pktdst = (string)$this->messages->first()->fqda->uplink;
$this->software['prodcode-lo'] = 0x00;
$this->software['prodcode-hi'] = 0xde;
$this->software['rev-maj'] = 0x00;
$this->software['rev-min'] = 0x01;
// Type 2+ Packet
$this->cap['valid'] = 0x0100;
$this->cap['word'] = 0x0001;
$this->pktver = 0x0002;
$return = $this->createHeader();
foreach ($this->messages as $o)
$return .= "\02\00".(string)$o;
$return .= "\00\00";
return $return;
}
/**
* Create our message packet header
*/
private function createHeader(): string
{
try {
$a = pack(join('',collect($this->pack1)->pluck(1)->toArray()),
$this->ff,
$this->tf,
$this->date->year,
$this->date->month,
$this->date->day,
$this->date->hour,
$this->date->minute,
$this->date->second,
$this->baud,
$this->pktver,
$this->fn, // @todo if point, this needs to be 0xff
$this->tn,
$this->software['prodcode-lo'], // @todo change to this software
$this->software['rev-maj'] // @todo change to this software
);
$b = pack(join('',collect($this->pack2)->pluck(1)->toArray()),
0x0000, // @note: Type 2 packet this is $this->sz,
0x0000, // @note: Type 2 packet this is $this->dz,
0x0000, // Filler $this->>sn if message to point.
$this->cap['valid'], // @todo to check
$this->software['prodcode-hi'], // @todo change to this software
$this->software['rev-min'], // @todo change to this software
$this->cap['word'], // @todo to check
$this->fz,
$this->tz,
$this->fp, // @note: point address, type 2+ packets
$this->tp // @note: point address, type 2+ packets
);
return $a.pack('a8',strtoupper($this->password)).$b."mbse"; // @todo change to this software
} catch (\Exception $e) {
return $e->getMessage();
}
}
public function addMessage(FTNMessage $o)
{
// @todo Check that this message is for the same uplink.
$this->messages->push($o);
}
public function dump()
{
return hex_dump((string)$this);
}
/**
* Open a packet file
*
* @param string $file
* @throws InvalidFidoPacketException
*/
private function OpenFile(string $file)
{
$f = fopen($file,'r');
// $fstat = fstat($f);
// PKT Header
$header = fread($f,0x3a);
// Could not read header
if (strlen($header) != 0x3a)
throw new InvalidFidoPacketException('Length of Header too short: '.$file);
// Not a type 2 packet
if (array_get(unpack('vv',substr($header,0x12)),'v') != 2)
throw new InvalidFidoPacketException('Not a type 2 packet in file: '. $file);
$this->parseHeader($header);
while (! feof($f))
{
$x = fread($f,2);
// End of Packet?
if (strlen($x) == 2 and $x == "\00\00")
{
break;
}
// Messages start with 02H 00H
if (strlen($x) == 2 AND $x != "\02\00")
throw new InvalidFidoPacketException('Not a valid packet: '.bin2hex($x));
// No message attached
else if (! strlen($x))
break;
$message = new FTNMessage(fread($f,0xc));
$message->date = Carbon::createFromFormat('d M y H:i:s',$this->readnullfield($f));
$message->to = $this->readnullfield($f);
$message->from = $this->readnullfield($f);
$message->subject = $this->readnullfield($f);
$message->parsemessage($this->readnullfield($f));
$this->messages->push($message);
}
}
private function readnullfield($f)
{
$result = '';
while (($x = fgetc($f) OR strlen($x)) AND $x !== "\00")
{
$result .= $x;
}
return $result;
}
private function parseHeader(string $header)
{
$result1 = unpack($this->unpackheader($this->pack1),substr($header,0,0x1a));
$this->password = array_get(unpack('a*p',substr($header,0x1a,8)),'p');
$result2 = unpack($this->unpackheader($this->pack2),substr($header,0x22,0x14));
$this->proddata = array_get(unpack('A*p',substr($header,0x36,4)),'p');
// @todo replcae these vars with the tz/fz
$this->sz = array_get($result2,'ozone');
$this->sn = array_get($result1,'onet');
$this->sf = array_get($result1,'onode');
$this->sp = array_get($result2,'dpoint');
$this->pktsrc = sprintf('%s:%s/%s.%s',
$this->sz,
$this->sn,
$this->sf,
$this->sp
);
$this->dz = array_get($result2,'dzone');
$this->dn = array_get($result1,'dnet');
$this->df = array_get($result1,'dnode');
$this->dp = array_get($result2,'dpoint');
$this->pktdst = sprintf('%s:%s/%s.%s',
$this->dz,
$this->dn,
$this->df,
$this->dp
);
$this->date = Carbon::create(
array_get($result1,'y'),
array_get($result1,'m'),
array_get($result1,'d'),
array_get($result1,'H'),
array_get($result1,'M'),
array_get($result1,'S')
);
$this->baud = array_get($result1,'baud');
$this->pktver = array_get($result1,'pktver');
$this->software['prodcode-lo'] = array_get($result1,'prodcode-lo');
$this->software['prodcode-hi'] = array_get($result2,'prodcode-hi');
$this->software['rev-maj'] = array_get($result1,'prodrev-maj');
$this->software['rev-min'] = array_get($result2,'prodrev-min');
$this->cap['valid'] = array_get($result2,'capvalid');
$this->cap['word'] = array_get($result2,'capword');
// @todo filler
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidFidoPacketException extends Exception
{
//
}

View File

@ -6,6 +6,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use App\Classes\FTN\Packet;
use App\Models\{Address,Domain,Setup};
class HomeController extends Controller
@ -27,6 +28,32 @@ class HomeController extends Controller
->with('user',Auth::user());
}
public function pkt(Request $request)
{
$pkt = NULL;
$file = NULL;
if ($request->post()) {
$request->validate([
'file' => 'required|filled|min:1',
]);
foreach ($request->allFiles() as $key => $filegroup) {
if ($key !== 'file')
continue;
foreach ($filegroup as $file) {
$pkt = new Packet($file);
break;
}
}
}
return view('pkt.debug')
->with('file',$file)
->with('result',$pkt);
}
public function search(Request $request): Collection
{
$this->middleware('auth');

View File

@ -86,13 +86,44 @@ class Address extends Model
* Find a record in the DB for a node string, eg: 10:1/1.0
*
* @param string $ftn
* @return Node|null
* @return Address|null
* @throws Exception
*/
public static function findFTN(string $ftn): ?self
{
$matches = [];
$ftn = self::parseFTN($ftn);
$o = (new self)->active()
->select('addresses.*')
->where('zones.zone_id',$ftn['z'])
->where('host_id',$ftn['h'])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where('addresses.active',TRUE)
->where('node_id',$ftn['f'])
->where('point_id',$ftn['p'])
->when($ftn['d'],function($query,$domain) {
$query->where('domains.name',$domain);
})
->when((! $ftn['d']),function($query) {
$query->where('domains.default',TRUE);
})
->single();
return ($o && $o->system->active) ? $o : NULL;
}
/**
* Parse a string and split it out as an FTN array
*
* @param string $ftn
* @return array
* @throws Exception
*/
public static function parseFTN(string $ftn): array
{
// http://ftsc.org/docs/frl-1028.002
if (! preg_match('#^([0-9]+):([0-9]+)/([0-9]+)(.([0-9]+))?(@([a-z0-9\-_~]{0,8}))?$#',strtolower($ftn),$matches))
throw new Exception('Invalid FTN: '.$ftn);
@ -102,29 +133,17 @@ class Address extends Model
if (! $matches[$i] || ($matches[$i] > DomainController::NUMBER_MAX))
throw new Exception('Invalid FTN: '.$ftn);
}
if (isset($matches[5]) AND $matches[5] > DomainController::NUMBER_MAX)
throw new Exception('Invalid FTN: '.$ftn);
$o = (new self)->active()
->select('addresses.*')
->where('zones.zone_id',$matches[1])
->where('host_id',$matches[2])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where('addresses.active',TRUE)
->where('node_id',$matches[3])
->where('point_id',(isset($matches[5]) AND $matches[5]) ? $matches[5] : 0)
->when(isset($matches[7]),function($query) use ($matches) {
$query->where('domains.name',$matches[7]);
})
->when((! isset($matches[7]) OR ! $matches[7]),function($query) {
$query->where('domains.default',TRUE);
})
->single();
return ($o && $o->system->active) ? $o : NULL;
return [
'z'=>(int)$matches[1],
'n'=>(int)$matches[2],
'f'=>(int)$matches[3],
'p'=>isset($matches[5]) && $matches[5] ? (int)$matches[5] : 0,
'd'=>$matches[7] ?? NULL
];
}
/**

View File

@ -4,6 +4,24 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\ScopeActive;
class Software extends Model
{
use ScopeActive;
public const SOFTWARE_MAILER = 0x01;
public const SOFTWARE_TOSSER = 0x02;
/* ATTRIBUTES */
public function getCodeAttribute($value)
{
return sprintf('%04X',$value);
}
public function getNameAttribute($value)
{
return $value ?: 'Unknown';
}
}

View File

@ -46,5 +46,17 @@ class AppServiceProvider extends ServiceProvider
return NULL;
});
// When a query should return 1 object, or NULL if it doesnt
Builder::macro('singleOrNew',function ($args) {
//dd(['func'=>func_get_args(),'args'=>$args,'this'=>$this]);
$result = $this->where($args)->get();
if ($result->count() == 1) {
return $result->first();
}
return $this->newModelInstance($args);
});
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddCodeToSoftware extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('software', function (Blueprint $table) {
$table->integer('code')->nullable(); // Mailer/Tosser Product Code
$table->integer('type'); // Mailer/Tosser
$table->unique(['code','type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('software', function (Blueprint $table) {
$table->dropUnique(['code','type']);
$table->dropColumn('code');
$table->dropColumn('type');
});
}
}

View File

@ -201,6 +201,7 @@ div#search_results ul {
color:#eeeeee;
background-color:#292929;
font-size: .85rem;
top:inherit !important;
}
div#search_results ul li.dropdown-header {
display: block;

View File

@ -22,4 +22,9 @@
<dd><a href="{{ url('network',['id'=>$o->id]) }}" title="{{ $o->description }}">{{ $o->name }}</a></dd>
@endforeach
</dl>
<dl>
<dt>Debug</dt>
<dd><a href="{{ url('pkt') }}">Verify Packet</a></dd>
</dl>
</div>

View File

@ -0,0 +1,123 @@
@extends('layouts.app')
@section('content')
<form class="row g-0 needs-validation" method="post" enctype="multipart/form-data" novalidate>
@csrf
<div class="row">
<div class="col-8">
<div class="greyframe titledbox shadow0xb0">
<h2 class="cap">Upload Packet for Analysis</h2>
<p class="small">This packet will NOT be processed.</p>
<div class="row">
<div class="col-12">
<label for="file" class="form-label">Packet</label>
<div class="input-group has-validation">
<input class="form-control @error('file') is-invalid @enderror" type="file" id="file" name="file[]" required>
<span class="invalid-feedback" role="alert">
@error('file')
{{ $message }}
@else
A file is required.
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<a href="{{ url('/') }}" class="btn btn-danger">Cancel</a>
<button type="submit" name="submit" class="btn btn-success mr-0 float-end">Process</button>
</div>
</div>
</div>
</div>
</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>
<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">
@foreach ($result->messages 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">
@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 class="accordion-body">
@if ($msg->errors)
@foreach ($msg->errors->messages()->all() as $error)
<div class="alert alert-danger">
{{ $error }}
</div>
@endforeach
@endif
<div class="row pb-2">
<div class="col-8">
<strong>DATE:</strong> {{ $msg->date }}
</div>
</div>
<div class="row pb-2">
<div class="col-4">
<strong>FROM:</strong> {{ $msg->user_from }} ({{ $msg->fftn }})
</div>
<div class="col-4">
<strong>TO:</strong> {{ $msg->user_to }} ({{ $msg->tftn }})
</div>
</div>
<div class="row pb-2">
<div class="col-8">
<strong>SUBJECT:</strong> {{ $msg->subject }}
</div>
</div>
<div class="row pb-2">
<div class="col-8">
<div class="pad pb-0">
<pre>{{ $msg->message }}</pre>
</div>
</div>
</div>
<div class="row pb-2">
<div class="col-8">
<strong>SEENBY:</strong> <br>{!! join('<br>',$msg->seenby->toArray()) !!}
</div>
</div>
<div class="row pb-2">
<div class="col-8">
<strong>PATH:</strong> <br>{!! join('<br>',$msg->path->toArray()) !!}
</div>
</div>
<div class="row pb-2">
<div class="col-8">
<strong>KLUDGES:</strong> <br>
@foreach ($msg->kludge as $k => $v)
<strong>{{ $k }}</strong> {{ $v }}<br>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
@endif
@endsection

View File

@ -231,7 +231,7 @@
<div id="collapse_ftnaddresses" class="accordion-collapse collapse" aria-labelledby="ftnaddress" data-bs-parent="#accordion_ftnaddress">
<div class="accordion-body">
<p>FidoNet system are also assigned some roles, and in some cases, those roles have a paritcular address format:</p>
<p>FidoNet system are also assigned some roles, and in some cases, those roles have a particular address format:</p>
<table class="table monotable">
<thead>
<tr>

View File

@ -52,6 +52,7 @@ Route::middleware(['verified','activeuser'])->group(function () {
Route::get('network/{o}',[HomeController::class,'network']);
Route::get('permissions',[HomeController::class,'permissions']);
Route::match(['get','post'],'pkt',[HomeController::class,'pkt']);
Route::get('search',[HomeController::class,'search']);
Route::middleware(['auth','can:admin'])->group(function () {