Added packet debug on web UI
This commit is contained in:
parent
dc86f7c008
commit
987b4040fb
@ -2,8 +2,34 @@
|
|||||||
|
|
||||||
namespace App\Classes;
|
namespace App\Classes;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
abstract class FTN
|
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.
|
* Determine if a line is a kludge line.
|
||||||
*
|
*
|
||||||
@ -20,12 +46,18 @@ abstract class FTN
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This function creates our unpack header
|
* This function creates our unpack header
|
||||||
|
*
|
||||||
|
* @param array $pack
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function unpackheader(array $pack)
|
protected function unpackheader(array $pack): string
|
||||||
{
|
{
|
||||||
return join('/',array_values(collect($pack)
|
return join('/',
|
||||||
->sortBy(function($k,$v) {return $k[0];})
|
collect($pack)
|
||||||
->transform(function($k,$v) {return $k[1].$v;})->toArray()));
|
->sortBy(function($k,$v) {return $k[0];})
|
||||||
|
->transform(function($k,$v) {return $k[1].$v;})
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
10
app/Classes/FTN/InvalidPacketException.php
Normal file
10
app/Classes/FTN/InvalidPacketException.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Classes\FTN;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class InvalidPacketException extends Exception
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
475
app/Classes/FTN/Message.php
Normal file
475
app/Classes/FTN/Message.php
Normal 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
308
app/Classes/FTN/Packet.php
Normal 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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class InvalidFidoPacketException extends Exception
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use App\Classes\FTN\Packet;
|
||||||
use App\Models\{Address,Domain,Setup};
|
use App\Models\{Address,Domain,Setup};
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
@ -27,6 +28,32 @@ class HomeController extends Controller
|
|||||||
->with('user',Auth::user());
|
->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
|
public function search(Request $request): Collection
|
||||||
{
|
{
|
||||||
$this->middleware('auth');
|
$this->middleware('auth');
|
||||||
|
@ -86,13 +86,44 @@ class Address extends Model
|
|||||||
* Find a record in the DB for a node string, eg: 10:1/1.0
|
* Find a record in the DB for a node string, eg: 10:1/1.0
|
||||||
*
|
*
|
||||||
* @param string $ftn
|
* @param string $ftn
|
||||||
* @return Node|null
|
* @return Address|null
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public static function findFTN(string $ftn): ?self
|
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
|
// 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))
|
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);
|
throw new Exception('Invalid FTN: '.$ftn);
|
||||||
@ -102,29 +133,17 @@ class Address extends Model
|
|||||||
if (! $matches[$i] || ($matches[$i] > DomainController::NUMBER_MAX))
|
if (! $matches[$i] || ($matches[$i] > DomainController::NUMBER_MAX))
|
||||||
throw new Exception('Invalid FTN: '.$ftn);
|
throw new Exception('Invalid FTN: '.$ftn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($matches[5]) AND $matches[5] > DomainController::NUMBER_MAX)
|
if (isset($matches[5]) AND $matches[5] > DomainController::NUMBER_MAX)
|
||||||
throw new Exception('Invalid FTN: '.$ftn);
|
throw new Exception('Invalid FTN: '.$ftn);
|
||||||
|
|
||||||
$o = (new self)->active()
|
return [
|
||||||
->select('addresses.*')
|
'z'=>(int)$matches[1],
|
||||||
->where('zones.zone_id',$matches[1])
|
'n'=>(int)$matches[2],
|
||||||
->where('host_id',$matches[2])
|
'f'=>(int)$matches[3],
|
||||||
->join('zones',['zones.id'=>'addresses.zone_id'])
|
'p'=>isset($matches[5]) && $matches[5] ? (int)$matches[5] : 0,
|
||||||
->join('domains',['domains.id'=>'zones.domain_id'])
|
'd'=>$matches[7] ?? NULL
|
||||||
->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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,24 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
use App\Traits\ScopeActive;
|
||||||
|
|
||||||
class Software extends Model
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,5 +46,17 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
return NULL;
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
1
public/oldschool/css/main.css
vendored
1
public/oldschool/css/main.css
vendored
@ -201,6 +201,7 @@ div#search_results ul {
|
|||||||
color:#eeeeee;
|
color:#eeeeee;
|
||||||
background-color:#292929;
|
background-color:#292929;
|
||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
|
top:inherit !important;
|
||||||
}
|
}
|
||||||
div#search_results ul li.dropdown-header {
|
div#search_results ul li.dropdown-header {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -22,4 +22,9 @@
|
|||||||
<dd><a href="{{ url('network',['id'=>$o->id]) }}" title="{{ $o->description }}">{{ $o->name }}</a></dd>
|
<dd><a href="{{ url('network',['id'=>$o->id]) }}" title="{{ $o->description }}">{{ $o->name }}</a></dd>
|
||||||
@endforeach
|
@endforeach
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Debug</dt>
|
||||||
|
<dd><a href="{{ url('pkt') }}">Verify Packet</a></dd>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
123
resources/views/pkt/debug.blade.php
Normal file
123
resources/views/pkt/debug.blade.php
Normal 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
|
@ -231,7 +231,7 @@
|
|||||||
|
|
||||||
<div id="collapse_ftnaddresses" class="accordion-collapse collapse" aria-labelledby="ftnaddress" data-bs-parent="#accordion_ftnaddress">
|
<div id="collapse_ftnaddresses" class="accordion-collapse collapse" aria-labelledby="ftnaddress" data-bs-parent="#accordion_ftnaddress">
|
||||||
<div class="accordion-body">
|
<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">
|
<table class="table monotable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -52,6 +52,7 @@ Route::middleware(['verified','activeuser'])->group(function () {
|
|||||||
|
|
||||||
Route::get('network/{o}',[HomeController::class,'network']);
|
Route::get('network/{o}',[HomeController::class,'network']);
|
||||||
Route::get('permissions',[HomeController::class,'permissions']);
|
Route::get('permissions',[HomeController::class,'permissions']);
|
||||||
|
Route::match(['get','post'],'pkt',[HomeController::class,'pkt']);
|
||||||
Route::get('search',[HomeController::class,'search']);
|
Route::get('search',[HomeController::class,'search']);
|
||||||
|
|
||||||
Route::middleware(['auth','can:admin'])->group(function () {
|
Route::middleware(['auth','can:admin'])->group(function () {
|
||||||
|
Loading…
Reference in New Issue
Block a user