FTN Packet inspection
This commit is contained in:
31
app/Classes/FTN.php
Normal file
31
app/Classes/FTN.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
abstract class FTN
|
||||
{
|
||||
/**
|
||||
* Determine if a line is a kludge line.
|
||||
*
|
||||
* @param string $kludge
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function kludge(string $kludge,string $string)
|
||||
{
|
||||
return (preg_match("/^{$kludge}/",$string))
|
||||
? chop(preg_replace("/^{$kludge}/",'',$string),"\r")
|
||||
: FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates our unpack header
|
||||
* @return string
|
||||
*/
|
||||
protected function unpackheader(array $pack)
|
||||
{
|
||||
return join('/',array_values(collect($pack)
|
||||
->sortBy(function($k,$v) {return $k[0];})
|
||||
->transform(function($k,$v) {return $k[1].$v;})->toArray()));
|
||||
}
|
||||
}
|
251
app/Classes/FTNMessage.php
Normal file
251
app/Classes/FTNMessage.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use App\Exceptions\InvalidFidoPacketException;
|
||||
|
||||
class FTNMessage extends FTN
|
||||
{
|
||||
private $src = NULL;
|
||||
private $dst = NULL;
|
||||
private $flags = NULL;
|
||||
private $cost = 0;
|
||||
|
||||
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 = [];
|
||||
|
||||
private $fqfa = NULL; // Fully qualified fidonet source where packet originated
|
||||
private $fqfd = NULL; // Fully qualified fidonet destination address (Netmail)
|
||||
|
||||
// Single value kludge items
|
||||
private $_kludge = [
|
||||
'chrs' => 'CHRS: ',
|
||||
'charset' => 'CHARSET: ',
|
||||
'codepage' => 'CODEPAGE: ',
|
||||
'pid' => 'PID: ',
|
||||
'tid' => 'TID: ',
|
||||
];
|
||||
|
||||
public function __construct(string $header)
|
||||
{
|
||||
// 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.
|
||||
|
||||
// FTS-0001.016 Message header 12 bytes
|
||||
// node, net, flags, cost
|
||||
$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],
|
||||
];
|
||||
|
||||
$result = unpack($this->unpackheader($struct),$header);
|
||||
|
||||
$this->src = sprintf('%s/%s',array_get($result,'onet'),array_get($result,'onode'));
|
||||
$this->dst = sprintf('%s/%s',array_get($result,'dnet'),array_get($result,'dnode'));
|
||||
$this->flags = array_get($result,'flags');
|
||||
$this->cost = array_get($result,'cost');
|
||||
}
|
||||
|
||||
public function __get($k)
|
||||
{
|
||||
return $this->{$k};
|
||||
}
|
||||
|
||||
public function __set($k,$v)
|
||||
{
|
||||
switch ($k)
|
||||
{
|
||||
case 'message':
|
||||
// Remove DOS \n\r
|
||||
$v = preg_replace("/\n\r/","\r",$v);
|
||||
|
||||
$this->parsemessage($v);
|
||||
break;
|
||||
|
||||
case 'origin':
|
||||
$this->parseorigin($v);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->{$k} = $v;
|
||||
}
|
||||
}
|
||||
|
||||
public function parsemessage(string $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->__set('origin',substr($v,$y));
|
||||
$matches = [];
|
||||
|
||||
preg_match('/^.*\((.*)\)$/',$this->origin,$matches);
|
||||
|
||||
if (($this->type() == 'Netmail' AND array_get($matches,1) != $this->fqfa) OR ! array_get($matches,1))
|
||||
throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->fqfa,array_get($matches,1)));
|
||||
|
||||
$this->fqfa = array_get($matches,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;
|
||||
}
|
||||
}
|
||||
|
||||
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->fqfd,$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
|
||||
*/
|
||||
public 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function description()
|
||||
{
|
||||
switch ($this->type())
|
||||
{
|
||||
case 'Echomail': return sprintf('Echomail: '.$this->echoarea);
|
||||
case 'Netmail': return sprintf('Netmail: %s->%s',$this->fqfa,$this->fqfd);
|
||||
default:
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
if ($this->echoarea)
|
||||
return 'Echomail';
|
||||
|
||||
if ($this->intl)
|
||||
return 'Netmail';
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
159
app/Classes/FTNPacket.php
Normal file
159
app/Classes/FTNPacket.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use App\Exceptions\InvalidFidoPacketException;
|
||||
|
||||
class FTNPacket extends FTN
|
||||
{
|
||||
public $pktsrc = NULL;
|
||||
public $pktdst = NULL;
|
||||
private $pktver = NULL;
|
||||
public $date = NULL;
|
||||
private $baud = NULL;
|
||||
private $proddata = NULL;
|
||||
private $password = NULL;
|
||||
|
||||
public $filename = NULL;
|
||||
public $messages = [];
|
||||
|
||||
public function __construct(string $file)
|
||||
{
|
||||
$this->filename = $file;
|
||||
|
||||
if ($file)
|
||||
return $this->OpenFile($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:'. $file);
|
||||
|
||||
$this->setHeader($header);
|
||||
$this->messages = collect();
|
||||
|
||||
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: '.$x);
|
||||
|
||||
// No message attached
|
||||
else if (! strlen($x))
|
||||
break;
|
||||
|
||||
$message = new FTNMessage(fread($f,0xc));
|
||||
$message->date = $this->readnullfield($f);
|
||||
$message->to = $this->readnullfield($f);
|
||||
$message->from = $this->readnullfield($f);
|
||||
$message->subject = $this->readnullfield($f);
|
||||
$message->message = $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;
|
||||
}
|
||||
|
||||
public function setHeader(string $header)
|
||||
{
|
||||
$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],
|
||||
];
|
||||
|
||||
$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],
|
||||
];
|
||||
|
||||
$result1 = unpack($this->unpackheader($pack1),substr($header,0,0x1a));
|
||||
$result2 = unpack($this->unpackheader($pack2),substr($header,0x22,0x14));
|
||||
|
||||
$this->pktsrc = sprintf('%s:%s/%s.%s',
|
||||
array_get($result2,'ozone'),
|
||||
array_get($result1,'onet'),
|
||||
array_get($result1,'onode'),
|
||||
array_get($result2,'dpoint')
|
||||
);
|
||||
|
||||
$this->pktdst = sprintf('%s:%s/%s.%s',
|
||||
array_get($result2,'dzone'),
|
||||
array_get($result1,'dnet'),
|
||||
array_get($result1,'dnode'),
|
||||
array_get($result2,'dpoint')
|
||||
);
|
||||
|
||||
$this->date = sprintf ('%d-%d-%d %d:%d:%d',
|
||||
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->password = array_get(unpack('A*p',substr($header,0x1a,8)),'p');
|
||||
$this->proddata = array_get(unpack('A*p',substr($header,0x36,4)),'p');
|
||||
}
|
||||
}
|
71
app/Console/Commands/FtnPkt.php
Normal file
71
app/Console/Commands/FtnPkt.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Classes\FTNPacket;
|
||||
|
||||
class FtnPkt extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ftn:pkt {file : Fidonet Packet File PKT} {--dump : Dump packet}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import Packet into Database';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$pkt = new FTNPacket($this->argument('file'));
|
||||
|
||||
$this->info(sprintf('Packet: %s has %s messages. Addr: %s->%s (Date: %s)',
|
||||
$pkt->filename,
|
||||
$pkt->messages->count(),
|
||||
$pkt->pktsrc,
|
||||
$pkt->pktdst,
|
||||
$pkt->date
|
||||
));
|
||||
|
||||
foreach ($pkt->messages as $o)
|
||||
{
|
||||
$this->warn(sprintf('-- From: %s(%s)->%s(%s), Type: %s, Size: %d, FQFA: %s',
|
||||
$o->from,
|
||||
$o->src,
|
||||
$o->to,
|
||||
$o->dst,
|
||||
$o->description(),
|
||||
strlen($o->message),
|
||||
$o->fqfa
|
||||
));
|
||||
|
||||
if ($o->unknown->count())
|
||||
$this->error(sprintf('?? %s Unknown headers',$o->unknown->count()));
|
||||
}
|
||||
|
||||
if ($this->option('dump'))
|
||||
dump($o);
|
||||
}
|
||||
}
|
10
app/Exceptions/InvalidFidoPacketException.php
Normal file
10
app/Exceptions/InvalidFidoPacketException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidFidoPacketException extends Exception
|
||||
{
|
||||
//
|
||||
}
|
Reference in New Issue
Block a user