From a0d3c8d8ab607c22994b0fdb0f270b417bd04f48 Mon Sep 17 00:00:00 2001 From: Deon George Date: Fri, 16 Jul 2021 00:54:23 +1000 Subject: [PATCH] Start of processing packets - implemented PING Responce to Netmail --- app/Classes/FTN.php | 10 +- app/Classes/FTN/Message.php | 576 +++++++++++------- app/Classes/FTN/Packet.php | 317 +++++----- app/Classes/FTN/Process.php | 84 +++ app/Classes/FTN/Process/Ping.php | 54 ++ app/Console/Commands/ImportPacket.php | 4 +- app/Console/Commands/ProcessPacket.php | 47 ++ app/Http/Controllers/HomeController.php | 4 +- app/Jobs/ProcessPacket.php | 78 +++ app/Models/Address.php | 55 +- app/Models/Netmail.php | 87 ++- app/Models/Setup.php | 5 + app/Models/System.php | 13 +- app/Traits/GetNode.php | 42 -- config/process.php | 7 + .../{seeds => seeders}/DatabaseSeeder.php | 0 .../{seeds => seeders}/InitialSetupSeeder.php | 0 database/seeders/NodeHierarchy.php | 208 +++++++ resources/views/about.blade.php | 3 +- resources/views/domain/view.blade.php | 4 +- tests/Feature/RoutingTest.php | 94 +++ tests/Feature/SiteAdminTest.php | 6 +- 22 files changed, 1256 insertions(+), 442 deletions(-) create mode 100644 app/Classes/FTN/Process.php create mode 100644 app/Classes/FTN/Process/Ping.php create mode 100644 app/Console/Commands/ProcessPacket.php create mode 100644 app/Jobs/ProcessPacket.php delete mode 100644 app/Traits/GetNode.php create mode 100644 config/process.php rename database/{seeds => seeders}/DatabaseSeeder.php (100%) rename database/{seeds => seeders}/InitialSetupSeeder.php (100%) create mode 100644 database/seeders/NodeHierarchy.php create mode 100644 tests/Feature/RoutingTest.php diff --git a/app/Classes/FTN.php b/app/Classes/FTN.php index b6b5a82..55e24f5 100644 --- a/app/Classes/FTN.php +++ b/app/Classes/FTN.php @@ -2,10 +2,12 @@ namespace App\Classes; -use Illuminate\Support\Arr; +use App\Models\Domain; abstract class FTN { + protected ?Domain $domain; // Domain the packet is from + public function __get($key) { switch ($key) { @@ -15,7 +17,7 @@ abstract class FTN $this->fn, $this->ff, $this->fp, - ); + ).($this->domain ? sprintf('@%s',$this->domain->name) : ''); case 'tftn': return sprintf('%d:%d/%d.%d', @@ -23,7 +25,7 @@ abstract class FTN $this->tn, $this->tf, $this->tp, - ); + ).($this->domain ? sprintf('@%s',$this->domain->name) : ''); default: throw new \Exception('Unknown key: '.$key); @@ -50,7 +52,7 @@ abstract class FTN * @param array $pack * @return string */ - protected function unpackheader(array $pack): string + protected static function unpackheader(array $pack): string { return join('/', collect($pack) diff --git a/app/Classes/FTN/Message.php b/app/Classes/FTN/Message.php index d65bda1..0ce0931 100644 --- a/app/Classes/FTN/Message.php +++ b/app/Classes/FTN/Message.php @@ -2,6 +2,7 @@ namespace App\Classes\FTN; +use Carbon\Carbon; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -9,9 +10,8 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Validator as ValidatorResult; use App\Classes\FTN as FTNBase; -use App\Models\Address; +use App\Models\{Address,Domain}; use App\Rules\TwoByteInteger; -use App\Traits\GetNode; /** * Class Message @@ -21,14 +21,15 @@ use App\Traits\GetNode; */ class Message extends FTNBase { - //use GetNode; + private const cast_utf8 = [ + 'message', + ]; // Single value kludge items private array $_kludge = [ 'chrs' => 'CHRS: ', 'charset' => 'CHARSET: ', 'codepage' => 'CODEPAGE: ', - 'msgid' => 'MSGID: ', 'pid' => 'PID: ', 'replyid' => 'REPLY: ', 'tid' => 'TID: ', @@ -63,7 +64,7 @@ class Message extends FTNBase '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 + '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 @@ -74,20 +75,23 @@ class Message extends FTNBase 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 $msgid; // MSG ID + private string $echoarea; // FTS-0004.001 + private string $intl; // Netmail details private string $message; // The actual message content + private string $tearline; 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 $point; // Point the message belongs to (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. // Convert characters into printable chars @@ -136,42 +140,86 @@ class Message extends FTNBase 0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0, ]; - public function __construct(string $msg) + public function __construct(Domain $domain=NULL) { + $this->domain = $domain; + $this->kludge = collect(); $this->path = collect(); $this->seenby = collect(); $this->via = collect(); - $this->_other = collect(); $this->unknown = collect(); $this->zone = []; $this->point = []; + $this->tearline = ''; + $this->origin = ''; + $this->msgid = ''; + $this->echoarea = ''; + $this->intl = ''; + } - $this->header = unpack($this->unpackheader(self::header),substr($msg,0,self::HEADER_LEN)); + /** + * Parse a message from a packet + * + * @param string $msg + * @param Domain|null $domain + * @return static + * @throws InvalidPacketException + */ + public static function parseMessage(string $msg,Domain $domain=NULL): self + { + $o = new self($domain); + + $o->header = unpack(self::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; + $o->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); + $ptr += strlen($o->user_to)+1; // From User - $this->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); - $ptr += strlen($this->user_from)+1; + $o->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); + $ptr += strlen($o->user_from)+1; // Subject - $this->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); - $ptr += strlen($this->subject)+1; + $o->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); + $ptr += strlen($o->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; + $o->echoarea = substr($msg,self::HEADER_LEN+$ptr+5,strpos($msg,"\r",self::HEADER_LEN+$ptr+5)-(self::HEADER_LEN+$ptr+5)); + $ptr += strlen($o->echoarea)+5+1; } - $this->parseMessage(substr($msg,self::HEADER_LEN+$ptr)); + $o->unpackMessage(substr($msg,self::HEADER_LEN+$ptr)); - if (($x=$this->validate()->getMessageBag())->count()) - Log::debug('Message fails validation',['result'=>$x]); + if (($x=$o->validate($domain))->fails()) { + Log::debug('Message fails validation',['result'=>$x->errors()]); + throw new \Exception('Message validation fails:'.join(' ',$x->errors()->all())); + } + + return $o; + } + + /** + * Translate the string into something printable via the web + * + * @param string $string + * @param array $skip + * @return string + */ + public static function tr(string $string,array $skip=[0x0a,0x0d]): string + { + $tr = []; + + foreach (self::CP437 as $k=>$v) { + if (in_array($k,$skip)) + continue; + + $tr[chr($k)] = '&#'.$v; + } + + return strtr($string,$tr); } public function __get($key) @@ -194,17 +242,19 @@ class Message extends FTNBase case 'tftn': return parent::__get($key); + case 'fftn_o': + return Address::findFTN($this->fftn); + case 'tftn_o': + return Address::findFTN($this->tftn); + case 'date': - return sprintf('%s (%s)',Arr::get($this->header,$key),$this->kludge->get('tzutc')); + return Carbon::createFromFormat('d M y H:i:s O', + sprintf('%s %s',chop(Arr::get($this->header,$key)),($x=$this->kludge->get('tzutc')) < 0 ? $x : '+'.$x)); case 'flags': case 'cost': return Arr::get($this->header,$key); - case 'msgid': return $this->kludge->get('msgid'); - case 'message': - return utf8_decode($this->{$key}); - case 'subject': case 'user_to': case 'user_from': @@ -212,29 +262,122 @@ class Message extends FTNBase case 'path': case 'seenby': case 'via': + case 'msgid': case 'errors': case 'echoarea': return $this->{$key}; - /* - case 'tearline': - return '--- FTNHub'; - */ + default: + throw new \Exception('Unknown key: '.$key); + } + } + + public function __set($key,$value) + { + switch ($key) { + case 'echoarea': + case 'header': + case 'intl': + case 'message': + case 'msgid': + case 'subject': + case 'user_from': + case 'user_to': + case 'via': + $this->{$key} = $value; + break; default: throw new \Exception('Unknown key: '.$key); } } + /** + * When we serialise this object, we'll need to utf8_encode some values + * + * @return array + */ + public function __serialize(): array + { + $values = []; + + $properties = (new \ReflectionClass($this))->getProperties(); + + $class = get_class($this); + + foreach ($properties as $property) { + if ($property->isStatic()) { + continue; + } + + $property->setAccessible(true); + + if (! $property->isInitialized($this)) { + continue; + } + + $name = $property->getName(); + $encode = in_array($name,self::cast_utf8); + + if ($property->isPrivate()) { + $name = "\0{$class}\0{$name}"; + } elseif ($property->isProtected()) { + $name = "\0*\0{$name}"; + } + + $property->setAccessible(true); + $value = $property->getValue($this); + + $values[$name] = $encode ? utf8_encode($value) : $value; + } + + return $values; + } + + /** + * When we unserialize, we'll restore (utf8_decode) some values + * + * @param array $values + */ + public function __unserialize(array $values): void + { + $properties = (new \ReflectionClass($this))->getProperties(); + + $class = get_class($this); + + foreach ($properties as $property) { + if ($property->isStatic()) { + continue; + } + + $name = $property->getName(); + $decode = in_array($name,self::cast_utf8); + + if ($property->isPrivate()) { + $name = "\0{$class}\0{$name}"; + } elseif ($property->isProtected()) { + $name = "\0*\0{$name}"; + } + + if (! array_key_exists($name, $values)) { + continue; + } + + $property->setAccessible(true); + + $property->setValue( + $this, $decode ? utf8_decode($values[$name]) : $values[$name] + ); + } + } + /** * 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()), @@ -243,48 +386,36 @@ class Message extends FTNBase $this->fn, $this->tn, $this->flags, - $this->cost + $this->cost, + $this->date->format('d M y H:i:s'), ); - // @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->user_to."\00"; + $return .= $this->user_from."\00"; $return .= $this->subject."\00"; - if ($this->type == 'echomail') + if ($this->isNetmail()) + $return .= sprintf("\01INTL %s\r",$this->intl); + else $return .= "AREA:".$this->echoarea."\r"; // Add some kludges - $return .= "\01MSGID ".$this->_fqfa." 1"."\r"; + $return .= sprintf("\01MSGID: %s\r",$this->msgid); foreach ($this->_kludge as $k=>$v) { if ($x=$this->kludge->get($k)) - $return .= chr(1).$v.$x."\r"; + $return .= sprintf("\01%s %s\r",$v,$x); } $return .= $this->message."\r"; - $return .= $this->tearline."\r"; - $return .= $this->origin."\r"; + if ($this->tearline) + $return .= $this->tearline."\r"; + if ($this->origin) + $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; + if ($this->isNetmail()) { + foreach ($this->via as $v) + $return .= sprintf("\01Via %s\r",$v); } $return .= "\00"; @@ -292,6 +423,16 @@ class Message extends FTNBase return $return; } + /** + * If this message doesnt have an AREATAG, then its a netmail. + * + * @return bool + */ + public function isNetmail(): bool + { + return ! $this->echoarea; + } + /** * Return an array of flag descriptions * @@ -299,26 +440,26 @@ class Message extends FTNBase * * 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 + * --- -------------------- + * 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 @@ -339,137 +480,6 @@ class Message extends FTNBase } */ - /** - * 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 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 .= utf8_encode(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']; - $this->point['src'] = $ftn['p']; - - // The message is the rest? - } elseif (strlen($v) > $x+1) { - $this->message .= utf8_encode(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: "FMPT - if ($t = $this->kludge('FMPT ',$v)) - $this->point['src'] = $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. - * - * "INTL "" " - */ - elseif ($t = $this->kludge('INTL ',$v)) { - $this->netmail['intl'] = $t; - - // INTL kludge is in Netmail, so we'll do some validation: - list($this->netmail['dst'],$this->netmail['src']) = explode(' ',$t); - - $src = Address::parseFTN($this->netmail['src']); - if (($src['n'] !== $this->fn) || ($src['f'] !== $this->ff)) { - Log::error(sprintf('INTL src address [%s] doesnt match packet',$this->netmail['src'])); - } else { - // We'll set our source zone - $this->zone['src'] = $src['z']; - } - - $dst = Address::parseFTN($this->netmail['dst']); - if (($dst['n'] !== $this->tn) || ($dst['f'] !== $this->tf)) { - Log::error(sprintf('INTL dst address [%s] doesnt match packet',$this->netmail['dst'])); - } else { - // We'll set our source zone - $this->zone['dst'] = $dst['z']; - } - } - - elseif ($t = $this->kludge('PATH: ',$v)) - $this->path->push($t); - - // To Point: TOPT - elseif ($t = $this->kludge('TOPT ',$v)) - $this->point['dst'] = $t; - - // Via @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] - 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 @@ -505,23 +515,128 @@ class Message extends FTNBase } /** - * Translate the string into something printable via the web + * Extract information out of the message text. * - * @param string $string - * @param array $skip - * @return string + * @param string $message + * @throws InvalidPacketException */ - public static function tr(string $string,array $skip=[0x0a,0x0d]): string + public function unpackMessage(string $message): void { - $tr = []; - foreach (self::CP437 as $k=>$v) { - if (in_array($k,$skip)) + // Remove DOS \n\r + $message = preg_replace("/\n\r/","\r",$message); + + // Split out the 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('No address in Origin?'); + + // 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]); + + // http://ftsc.org/docs/fsc-0068.001 + // MSGID should be the basis of the source + $this->zone['src'] = $ftn['z']; + $this->point['src'] = $ftn['p']; + + // 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; - $tr[chr($k)] = '&#'.$v; - } + // From point: "FMPT + if ($t = $this->kludge('FMPT ',$v)) + $this->point['src'] = $t; - return strtr($string,$tr); + /* + * 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. + * + * "INTL "" " + */ + elseif ($t = $this->kludge('INTL ',$v)) { + $this->intl = $t; + + // INTL kludge is in Netmail, so we'll do some validation: + list($dst,$src) = explode(' ',$t); + + $ftn = Address::parseFTN($src); + if (($ftn['n'] !== $this->fn) || ($ftn['f'] !== $this->ff)) { + Log::error(sprintf('INTL src address [%s] doesnt match packet',$src)); + + } else { + // We'll set our source zone + $this->zone['src'] = $ftn['z']; + } + + $ftn = Address::parseFTN($dst); + if (($ftn['n'] !== $this->tn) || ($ftn['f'] !== $this->tf)) { + Log::error(sprintf('INTL dst address [%s] doesnt match packet',$dst)); + + } else { + // We'll set our source zone + $this->zone['dst'] = $ftn['z']; + } + } + + elseif ($t = $this->kludge('MSGID: ',$v)) + $this->msgid = $t; + + elseif ($t = $this->kludge('PATH: ',$v)) + $this->path->push($t); + + // To Point: TOPT + elseif ($t = $this->kludge('TOPT ',$v)) + $this->point['dst'] = $t; + + // Via @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] + 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")); + } } /** @@ -529,7 +644,7 @@ class Message extends FTNBase * * @return \Illuminate\Contracts\Validation\Validator */ - private function validate(): ValidatorResult + public function validate(Domain $domain=NULL): ValidatorResult { // Check lengths $validator = Validator::make([ @@ -543,6 +658,8 @@ class Message extends FTNBase 'flags' => $this->flags, 'cost' => $this->cost, 'echoarea' => $this->echoarea, + 'ozone' => $this->fz, + 'dzone' => $this->tz, ],[ 'user_from' => 'required|min:1|max:'.self::USER_FROM_LEN, 'user_to' => 'required|min:1|max:'.self::USER_TO_LEN, @@ -554,8 +671,19 @@ class Message extends FTNBase 'flags' => 'required|numeric', 'cost' => 'required|numeric', 'echoarea' => 'nullable|max:'.self::AREATAG_LEN, + 'ozone' => ['required',$this->domain ? 'in:'.$x=join(',',$this->domain->zones->pluck('zone_id')->toArray()): ''], + 'dzone' => ['required',$this->domain ? 'in:'.$x : ''] ]); + if ($domain) { + $validator->after(function($validator) { + if (! Address::findFTN($this->fftn)) + $validator->errors()->add('from',sprintf('Undefined Node [%s] sent packet.',$this->fftn)); + if (! Address::findFTN($this->tftn)) + $validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->fftn)); + }); + } + if ($validator->fails()) $this->errors = $validator; diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index 1f58dad..24e6890 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -9,12 +9,10 @@ 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; +use App\Models\{Address,Domain,Setup,Software}; class Packet extends FTNBase { - //use GetNode; private const LOGKEY = 'PKT'; private const HEADER_LEN = 0x3a; @@ -22,10 +20,6 @@ class Packet extends FTNBase 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 @@ -54,17 +48,113 @@ class Packet extends FTNBase '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 + 'proddata' => [0x36,'a4',4], // ProdData (Not used 2) // FSC-39/FSC-48 ]; - public function __construct(File $file) + private array $header; // Packet Header + + public File $file; // Packet filename + public Collection $messages; // Messages in the Packet + + public function __construct(Address $o=NULL) { $this->messages = collect(); + $this->domain = NULL; - if ($file) { - $this->file = $file; - $this->open($file); + // If we are creating an outbound packet, we need to set our header + if ($o) + $this->newHeader($o); + } + + /** + * Open a packet file + * + * @param File $file + * @param Domain|null $domain + * @return Packet + * @throws InvalidPacketException + */ + public static function open(File $file,Domain $domain=NULL): self + { + 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); + + $o = new self; + $o->header = unpack(self::unpackheader(self::v2header),$header); + + $x = fread($f,2); + + // End of Packet? + if (strlen($x) == 2 and $x == "\00\00") + return new self; + + // 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+3; + + if ($buf_ptr >= strlen($readbuf)) + $buf_ptr = 0; + } + + // Look for the next message + $o->parseMessage($message,$domain); + $message = ''; } + + return $o; } /** @@ -132,31 +222,14 @@ class Packet extends FTNBase } } - // @note - messages in this object have the same next destination - // @todo To rework - /* + /** + * Return the packet + * + * @return string + * @throws \Exception + */ 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) @@ -166,143 +239,101 @@ class Packet extends FTNBase return $return; } - */ /** * Create our message packet header - * @todo To rework */ - /* private function createHeader(): string { try { - $a = pack(join('',collect($this->pack1)->pluck(1)->toArray()), + $a = pack(join('',collect(self::v2header)->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 + Arr::get($this->header,'y'), + Arr::get($this->header,'m'), + Arr::get($this->header,'d'), + Arr::get($this->header,'H'), + Arr::get($this->header,'M'), + Arr::get($this->header,'S'), + Arr::get($this->header,'baud',0), + Arr::get($this->header,'pktver',2), + $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 + Arr::get($this->header,'prodcode-lo',(Setup::PRODUCT_ID & 0xff)), + Arr::get($this->header,'prodrev-maj',Setup::PRODUCT_VERSION_MAJ), + $this->password, $this->fz, $this->tz, - $this->fp, // @note: point address, type 2+ packets - $this->tp // @note: point address, type 2+ packets + Arr::get($this->header,'filler',''), + Arr::get($this->header,'capvalid',1<<0), + Arr::get($this->header,'prodcode-hi',(Setup::PRODUCT_ID >> 8) & 0xff), + Arr::get($this->header,'prodrev-min',Setup::PRODUCT_VERSION_MIN), + Arr::get($this->header,'capword',1<<0), + $this->fz, + $this->tz, + $this->fp, + $this->tp, + Arr::get($this->header,'proddata','AB8D'), ); - return $a.pack('a8',strtoupper($this->password)).$b."mbse"; // @todo change to this software + return $a; } catch (\Exception $e) { return $e->getMessage(); } } - public function addMessage(FTNMessage $o) + /** + * Add a netmail message to this packet + * + * @param Message $o + */ + public function addNetmail(Message $o): void { - // @todo Check that this message is for the same uplink. $this->messages->push($o); } - */ /** - * Open a packet file + * When creating a new packet, set the header. * - * @param string $file - * @throws InvalidPacketException + * @param array $header */ - private function open(string $file) + private function newHeader(Address $o): void { - Log::debug(sprintf('%s:Opening Packet [%s]',self::LOGKEY,$file)); + $date = Carbon::now(); + $ao = Setup::findOrFail(config('app.id'))->system->match($o); - $f = fopen($file,'r'); - $fstat = fstat($f); + // Create Header + $this->header = [ + 'onode' => $ao->node_id, // Originating Node + 'dnode' => $o->node_id, // Destination Node + 'y' => $date->format('Y'), // Year + 'm' => $date->format('m')-1, // Month + 'd' => $date->format('d'), // Day + 'H' => $date->format('H'), // Hour + 'M' => $date->format('i'), // Minute + 'S' => $date->format('s'), // Second + 'onet' => $ao->host_id ?: $ao->region_id, // Originating Net (0xffff when origPoint !=0 2+) + 'dnet' => $o->host_id ?: $o->region_id, // Destination Net + 'password' => $o->session('pktpass'), // Packet Password + 'qozone' => $ao->zone->zone_id, + 'qdzone' => $o->zone->zone_id, + 'ozone' => $ao->zone->zone_id, // Originating Zone (Not used 2) + 'dzone' => $o->zone->zone_id, // Destination Zone (Not used 2) + 'opoint' => $ao->point_id, // Originating Point (Not used 2) + 'dpoint' => $o->point_id, // Destination Point (Not used 2) + ]; + } - // 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 = ''; - } + /** + * Parse a message in a mail packet + * + * @param string $message + * @param Domain $domain + * @throws \Exception + */ + public function parseMessage(string $message,Domain $domain=NULL): void + { + $this->messages->push(Message::parseMessage($message,$domain)); } } \ No newline at end of file diff --git a/app/Classes/FTN/Process.php b/app/Classes/FTN/Process.php new file mode 100644 index 0000000..43b92b7 --- /dev/null +++ b/app/Classes/FTN/Process.php @@ -0,0 +1,84 @@ +user_to) !== 'ping') + return FALSE; + + $reply = sprintf("Your ping was received here on %s and it took %s to get here.\n", + Carbon::now()->toDateTimeString(), + $msg->date->diffForHumans(['parts'=>3,'syntax'=>CarbonInterface::DIFF_ABSOLUTE]) + ); + + $reply .= "\n"; + $reply .= "Your message travelled along this path to get here:\n"; + foreach ($msg->via as $path) + $reply .= sprintf(" * %s\n",$path); + + $o = new Netmail(); + $o->to = $msg->user_from; + $o->from = Setup::PRODUCT_NAME; + $o->subject = 'Ping Reply'; + $o->fftn_id = ($x=$msg->tftn_o) ? $x->id : NULL; + $o->tftn_id = ($x=$msg->fftn_o) ? $x->id : NULL; + $o->msg = static::format_msg($reply); + $o->reply = $msg->msgid; + + $o->tagline = '... My ping pong opponent was not happy with my serve. He kept returning it.'; + $o->tearline = sprintf('--- %s (%s)',Setup::PRODUCT_NAME,(new Setup)->version); + $o->save(); + + return TRUE; + } +} \ No newline at end of file diff --git a/app/Console/Commands/ImportPacket.php b/app/Console/Commands/ImportPacket.php index 4da1f9d..6599ea2 100644 --- a/app/Console/Commands/ImportPacket.php +++ b/app/Console/Commands/ImportPacket.php @@ -4,13 +4,13 @@ namespace App\Console\Commands; use Illuminate\Console\Command; -use App\Traits\{GetNode,ParseNodes}; +use App\Traits\ParseNodes; use App\Classes\FTNPacket; use App\Models\{Echomail,Netmail,Zone}; class ImportPacket extends Command { - use GetNode,ParseNodes; + use ParseNodes; /** * The name and signature of the console command. diff --git a/app/Console/Commands/ProcessPacket.php b/app/Console/Commands/ProcessPacket.php new file mode 100644 index 0000000..2007eb2 --- /dev/null +++ b/app/Console/Commands/ProcessPacket.php @@ -0,0 +1,47 @@ +argument('pkt')); + $d = Domain::where('name',$this->argument('domain'))->singleOrFail(); + + foreach ((Packet::open($f,$d))->messages as $msg) { + // @todo Quick check that the packet should be processed by us. + // @todo validate that the packet's zone is in the domain. + + // Dispatch job. + Job::dispatchSync($msg); + } + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index ab58ba5..156645f 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -51,10 +51,10 @@ class HomeController extends Controller foreach ($filegroup as $file) { try { - $pkt = new Packet($file); + $pkt = Packet::open($file); } catch (\Exception $e) { - return redirect()->back()->withErrors($e->getMessage()); + return redirect()->back()->withErrors(sprintf('%s (%s:%d)',$e->getMessage(),$e->getFile(),$e->getLine())); } break; diff --git a/app/Jobs/ProcessPacket.php b/app/Jobs/ProcessPacket.php new file mode 100644 index 0000000..3ae8351 --- /dev/null +++ b/app/Jobs/ProcessPacket.php @@ -0,0 +1,78 @@ +msg = $msg; + } + + /** + * When calling ProcessPacket - we assume that the packet is from a valid source + */ + public function handle() + { + // Load our details + $ftns = Setup::findOrFail(config('app.id'))->system->addresses; + + // If we are a netmail + if ($this->msg->isNetmail()) { + // @todo Enable checks to reject old messages + + // Determine if the message is to this system, or in transit + if ($ftns->search(function($item) { dump($item->ftn);return $this->msg->tftn == $item->ftn; }) !== FALSE) { + // @todo Check if it is a duplicate message + // @todo Check if the message is from a system we know about + + $processed = FALSE; + + // If the message is to a bot, we'll process it + foreach (config('process.robots') as $class) { + if ($processed = $class::handle($this->msg)) { + break; + } + } + + // If not processed, no users here! + if (! $processed) { + dump('message not processed, no users here'); + } + + // If in transit, store for collection + } else { + // @todo Check if the message is to a system we know about + // @todo In transit loop checking + // @todo In transit TRACE response + + dump('netmail in transit'); + } + + // Else we are echomail + } else { + dump('echomail'); + // Determine if we know about this echo area + // Can the sender create it if it doesnt exist? + // Create it, or + + // Else record in bad area + + // We know about this area, store it + } + } +} \ No newline at end of file diff --git a/app/Models/Address.php b/app/Models/Address.php index 956a84e..59eb96d 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -6,6 +6,7 @@ use Exception; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use App\Classes\FTN\Packet; use App\Http\Controllers\DomainController; use App\Traits\ScopeActive; @@ -28,13 +29,35 @@ class Address extends Model /** * Find children dependant on this record - * - * @todo While this is finding children of hubs, we are not currently finding children of Hosts or Regions. - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function children() { - return $this->belongsTo(self::class,'id','hub_id'); + switch (strtolower($this->role)) { + case 'region': + return $this->hasMany(self::class,'region_id','region_id') + ->where('zone_id',$this->zone_id) + ->where(function($q) { + return $q->where('host_id',0) + ->orWhere('role',DomainController::NODE_NC); + }) + ->where('id','<>',$this->id); + + case 'host': + return $this->hasMany(self::class,'host_id','host_id') + ->where('zone_id',$this->zone_id) + ->where('region_id',$this->region_id) + ->whereNull('hub_id') + ->where('id','<>',$this->id); + + case 'hub': + return $this->hasMany(self::class,'hub_id','id'); + + case 'node': + return NULL; + + default: + throw new Exception('Unknown role: '.$this->role); + } } public function system() @@ -54,17 +77,17 @@ class Address extends Model * * @return string */ - public function getFTNAttribute() + public function getFTNAttribute(): string { return sprintf('%s@%s',$this->getFTN4DAttribute(),$this->zone->domain->name); } - public function getFTN3DAttribute() + public function getFTN3DAttribute(): string { return sprintf('%d:%d/%d',$this->zone->zone_id,$this->host_id ?: $this->region_id,$this->node_id); } - public function getFTN4DAttribute() + public function getFTN4DAttribute(): string { return sprintf('%s.%d',$this->getFTN3DAttribute(),$this->point_id); } @@ -91,7 +114,7 @@ class Address extends Model } } - /* GENERAL METHODS */ + /* METHODS */ /** * Find a record in the DB for a node string, eg: 10:1/1.0 @@ -126,6 +149,22 @@ class Address extends Model return ($o && $o->system->active) ? $o : NULL; } + /** + * Get netmail for this node (including it's children) + */ + public function getNetmail(): Packet + { + $o = new Packet($this); + + foreach (Netmail::whereIn('tftn_id',$this->children->pluck('id')->push($this->id))->get() as $oo) { + $o->addNetmail($oo->packet()); + + // @todo We need to mark the netmail as sent + } + + return $o; + } + /** * Parse a string and split it out as an FTN array * diff --git a/app/Models/Netmail.php b/app/Models/Netmail.php index a96d302..5beabc3 100644 --- a/app/Models/Netmail.php +++ b/app/Models/Netmail.php @@ -2,21 +2,92 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; +use Carbon\Carbon; +use Jenssegers\Mongodb\Eloquent\Model; +use Jenssegers\Mongodb\Eloquent\SoftDeletes; + +use App\Classes\FTN\Message; class Netmail extends Model { - protected $dates = ['date']; - protected $fillable = ['date','msgid','from_ftn']; - public $timestamps = FALSE; // @todo Remove, seems an issue with cockroach updating tables. + use SoftDeletes; - public function kludges() + protected $connection = 'mongodb'; + protected $dates = ['datetime']; + + /* RELATIONS */ + + public function fftn() { - return $this->belongsToMany(Kludge::class); + return $this + ->setConnection('pgsql') + ->belongsTo(Address::class); } - public function paths() + public function tftn() { - return $this->belongsToMany(Path::class,NULL,NULL,'node_id'); + return $this + ->setConnection('pgsql') + ->belongsTo(Address::class); + } + + /* ATTRIBUTES */ + + public function getMsgAttribute($value): string + { + return utf8_decode($value); + } + + public function setMsgAttribute($value): void + { + $this->attributes['msg'] = utf8_encode($value); + } + + /* METHODS */ + + /** + * Return this model as a packet + */ + public function packet(): Message + { + $o = new Message; + + $o->header = [ + 'onode' => $this->fftn->node_id, + 'dnode' => $this->tftn->node_id, + 'onet' => $this->fftn->host_id, + 'dnet' => $this->tftn->host_id, + 'flags' => 0, // @todo? + 'cost' => 0, + 'date'=>$this->created_at->format('d M y H:i:s'), + ]; + + $o->user_to = $this->to; + $o->user_from = $this->from; + $o->subject = $this->subject; + $o->message = $this->msg; + + $o->msgid = sprintf('%s %08x',$this->fftn->ftn3d,crc32($this->id)); + + // VIA kludge + $via = $this->via ?: collect(); + $via->push( + sprintf('%s @%s.UTC %s %d.%d/%s %s', + $this->fftn->ftn3d, + Carbon::now()->utc()->format('Ymd.His'), + Setup::PRODUCT_NAME, + Setup::PRODUCT_VERSION_MAJ, + Setup::PRODUCT_VERSION_MIN, + (new Setup)->version, + Carbon::now()->format('Y-m-d'), + )); + + $o->via = $via; + + // INTL kludge + // @todo Point handling FMPT/TOPT + $o->intl = sprintf('%s %s',$this->tftn->ftn3d,$this->fftn->ftn3d); + + return $o; } } \ No newline at end of file diff --git a/app/Models/Setup.php b/app/Models/Setup.php index 7ed4152..77826c8 100644 --- a/app/Models/Setup.php +++ b/app/Models/Setup.php @@ -37,6 +37,11 @@ class Setup extends Model public const O_EMSI = 1<<2; /* Listen for EMSI connections */ public const O_HIDEAKA = 1<<3; /* Hide AKAs to different Zones */ + public const PRODUCT_NAME = 'Clearing Houz'; + public const PRODUCT_ID = 0xAB8D; + public const PRODUCT_VERSION_MAJ = 0; + public const PRODUCT_VERSION_MIN = 0; + // Our non model attributes and values private array $internal = []; diff --git a/app/Models/System.php b/app/Models/System.php index 8a3debf..34353c4 100644 --- a/app/Models/System.php +++ b/app/Models/System.php @@ -56,7 +56,18 @@ class System extends Model return $this->hasManyThrough(Zone::class,Address::class,'system_id','id','id','zone_id'); } - /* GENERAL METHODS */ + /* METHODS */ + + /** + * Return the system's address in the same zone + * + * @param Address $o + * @return Address + */ + public function match(Address $o): Address + { + return $this->addresses->where('zone_id',$o->zone_id)->first(); + } /** * Return the system name, or role name for the zone diff --git a/app/Traits/GetNode.php b/app/Traits/GetNode.php deleted file mode 100644 index 4133637..0000000 --- a/app/Traits/GetNode.php +++ /dev/null @@ -1,42 +0,0 @@ -$z]); - - $no = Node::firstOrNew([ - 'zone_id'=>$zo->id, - 'host_id'=>Arr::get($address,'n'), - 'node_id'=>Arr::get($address,'f'), - 'point_id'=>Arr::get($address,'p',0) - ]); - - if (! $no->exists AND $create) - { - $no->active = FALSE; - $no->system = 'AUTO DISCOVERED'; - $no->sysop = 'UNKNOWN'; - $no->location = ''; - $no->baud = 0; - - $no->save(); - } - - return ($no->exists) ? $no : NULL; - } -} \ No newline at end of file diff --git a/config/process.php b/config/process.php new file mode 100644 index 0000000..bc26578 --- /dev/null +++ b/config/process.php @@ -0,0 +1,7 @@ + [ + \App\Classes\FTN\Process\Ping::class, + ], +]; \ No newline at end of file diff --git a/database/seeds/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php similarity index 100% rename from database/seeds/DatabaseSeeder.php rename to database/seeders/DatabaseSeeder.php diff --git a/database/seeds/InitialSetupSeeder.php b/database/seeders/InitialSetupSeeder.php similarity index 100% rename from database/seeds/InitialSetupSeeder.php rename to database/seeders/InitialSetupSeeder.php diff --git a/database/seeders/NodeHierarchy.php b/database/seeders/NodeHierarchy.php new file mode 100644 index 0000000..6c8a74d --- /dev/null +++ b/database/seeders/NodeHierarchy.php @@ -0,0 +1,208 @@ +insert([ + 'name'=>'Domain A', + 'active'=>TRUE, + 'public'=>TRUE, + 'default'=>FALSE, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + + DB::table('domains') + ->insert([ + 'name'=>'Domain B', + 'active'=>TRUE, + 'public'=>TRUE, + 'default'=>FALSE, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + + foreach (['Domain A','Domain B'] as $domain) { + $domain = Domain::where('name',$domain)->singleOrFail(); + $this->hierarchy($domain,100); + $this->hierarchy($domain,101); + } + } + + private function hierarchy(Domain $domain,int $zoneid) + { + $regions = [1,2]; + $hosts = [0,1]; + $hubs = [1000,2000]; + $nodes = [1,2,3]; + $hubnodes = [-1,+1]; + + $so = $this->system('ZC '.$domain->id); + DB::table('zones') + ->insert([ + 'zone_id'=>$zoneid, + 'active'=>TRUE, + 'notes'=>sprintf('Zone: %03d:0/0.0@%s',$zoneid,$domain->name), + 'domain_id'=>$domain->id, + 'system_id'=>$so->id, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + + $zo = Zone::where('zone_id',$zoneid)->where('domain_id',$domain->id)->singleOrFail(); + + // Nodes + foreach ($nodes as $nid) { + $so = $this->system(sprintf('Node %03d:%03d/%03d.0@%s (ZC Node)',$zoneid,0,$nid,$domain->name)); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>0, + 'host_id'=>0, + 'node_id'=>$nid, + 'point_id'=>0, + 'system_id'=>$so->id, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + } + + // Regions + foreach ($regions as $rid) { + $so = $this->system(sprintf('Region %03d:%03d/%03d.0@%s',$zoneid,$rid,0,$domain->name)); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>$rid, + 'host_id'=>0, + 'node_id'=>0, + 'point_id'=>0, + 'system_id'=>$so->id, + 'role'=>DomainController::NODE_RC, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + + // Nodes + foreach ($nodes as $nid) { + $so = $this->system(sprintf('Node %03d:%03d/%03d.0@%s (RC Node)',$zoneid,$rid,$nid,$domain->name)); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>$rid, + 'host_id'=>0, + 'node_id'=>$nid, + 'point_id'=>0, + 'system_id'=>$so->id, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + } + + // Hosts + foreach ($hosts as $hid) { + $hostid = $rid*100+$hid; + $so = $this->system(sprintf('Host %03d:%03d/0.0@%s (Region %03d)',$zoneid,$hostid,$domain->name,$rid)); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>$rid, + 'host_id'=>$hostid, + 'node_id'=>0, + 'point_id'=>0, + 'system_id'=>$so->id, + 'role'=>DomainController::NODE_NC, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + + // Nodes + foreach ($nodes as $nid) { + $so = $this->system(sprintf('Node %03d:%03d/%03d.0@%s (Region %03d) - Host Node',$zoneid,$hostid,$nid,$domain->name,$rid)); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>$rid, + 'host_id'=>$hostid, + 'node_id'=>$nid, + 'point_id'=>0, + 'system_id'=>$so->id, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + } + + // Hubs + foreach ($hubs as $bid) { + $so = $this->system(sprintf('Hub %03d:%03d/%03d.0@%s (Region %03d)',$zoneid,$hostid,$bid,$domain->name,$rid)); + $hub = new Address; + $hub->zone_id = $zo->id; + $hub->active = TRUE; + $hub->region_id = $rid; + $hub->host_id = $hostid; + $hub->node_id = $bid; + $hub->point_id = 0; + $hub->system_id = $so->id; + $hub->role = DomainController::NODE_HC; + $hub->created_at = Carbon::now(); + $hub->updated_at = Carbon::now(); + $hub->save(); + + // Nodes + foreach ($hubnodes as $nid) { + $nodeid = $bid+$nid; + $so = $this->system(sprintf('Node %03d:%03d/%03d.0@%s (Region %03d) - Hub Node',$zoneid,$hostid,$nodeid,$domain->name,$rid)); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>$rid, + 'host_id'=>$hostid, + 'node_id'=>$nodeid, + 'point_id'=>0, + 'system_id'=>$so->id, + 'hub_id'=>$hub->id, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); + } + } + } + } + } + + private function system(string $name): System + { + $o = new System; + $o->name = $name; + $o->sysop = 'Mr Sysop of '.$name; + $o->location = 'Some place for '.$name; + $o->active = TRUE; + $o->created_at = Carbon::now(); + $o->updated_at = Carbon::now(); + $o->save(); + + return $o; + } +} diff --git a/resources/views/about.blade.php b/resources/views/about.blade.php index 1571c8d..a9c2d3a 100644 --- a/resources/views/about.blade.php +++ b/resources/views/about.blade.php @@ -28,8 +28,7 @@
  • Supports BINKP network transfers
  • Supports EMSI network transfers
  • -
  • Supports PING responses To be implemented
  • -
  • Proxy mode, if you want your BBS to have our main address To be implemented
  • +
  • Supports PING responses
  • A consistent reliable echomail/netmail hub for your BBSes.
    If you have more than 1 BBS, then the Clearing House can receive all your mail from your uplinks and feed them to your BBSes.
  • diff --git a/resources/views/domain/view.blade.php b/resources/views/domain/view.blade.php index 1e53b11..3508ca8 100644 --- a/resources/views/domain/view.blade.php +++ b/resources/views/domain/view.blade.php @@ -156,9 +156,7 @@ paging: true, pageLength: 25, searching: true, - order: [ - [3,'asc'], - ], + order: [], }); @append \ No newline at end of file diff --git a/tests/Feature/RoutingTest.php b/tests/Feature/RoutingTest.php new file mode 100644 index 0000000..c359e5e --- /dev/null +++ b/tests/Feature/RoutingTest.php @@ -0,0 +1,94 @@ +singleOrFail(); + $zo = $do->zones->where('zone_id',100)->pop(); + return $zo->addresses; + } + + /** + * Test the ZC address. + * + * @return void + */ + public function test_zc() + { + $nodes = $this->zone(); + $this->assertEquals(51,$nodes->count()); + + /* + * ZCs addresses are not in the address table, so we cannot workout children + $zc = $nodes->where('role',DomainController::NODE_ZC); + $this->assertEquals(1,$zc->count()); + $zc = $zc->pop(); + + // ZC has 2 Region and 3 nodes as children + $this->assertEquals(5,$zc->children()); + */ + } + + /** + * Test the RC address. + * + * @return void + */ + public function test_rc() + { + $nodes = $this->zone(); + $rc = $nodes->where('role','Region'); + $this->assertEquals(2,$rc->count()); + + // First RC + $rc = $rc->pop(); + + // RC has 3 nodes and 2 hosts as children + $this->assertEquals(5,$rc->children->count()); + } + + /** + * Test the NC address. + * + * @return void + */ + public function test_nc() + { + $nodes = $this->zone(); + $nc = $nodes->where('role','Host'); + $this->assertEquals(4,$nc->count()); + + // First NC + $nc = $nc->pop(); + + // NC has 3 nodes and 2 hubs as children + $this->assertEquals(5,$nc->children->count()); + } + + /** + * Test the HC address. + * + * @return void + */ + public function test_hc() + { + $nodes = $this->zone(); + $hc = $nodes->where('role','Hub'); + //dd($hc->pluck('ftn')); + $this->assertEquals(8,$hc->count()); + + // First HC + $hc = $hc->pop(); + + // HC has 2 children + $this->assertEquals(2,$hc->children->count()); + } +} diff --git a/tests/Feature/SiteAdminTest.php b/tests/Feature/SiteAdminTest.php index 4d6645f..4b43565 100644 --- a/tests/Feature/SiteAdminTest.php +++ b/tests/Feature/SiteAdminTest.php @@ -28,15 +28,15 @@ class SiteAdminTest extends TestCase $this->get('ftn/zone') ->assertRedirect('login'); - $this->get('network/1') + $this->get('network/999') ->assertNotFound(); Domain::factory()->create([ - 'id'=>1, + 'id'=>999, 'name'=>'test', ]); - $this->get('network/1') + $this->get('network/999') ->assertOK(); }