diff --git a/app/Classes/FTN.php b/app/Classes/FTN.php index 46b1045..b6b5a82 100644 --- a/app/Classes/FTN.php +++ b/app/Classes/FTN.php @@ -2,8 +2,34 @@ namespace App\Classes; +use Illuminate\Support\Arr; + abstract class FTN { + public function __get($key) + { + switch ($key) { + case 'fftn': + return sprintf('%d:%d/%d.%d', + $this->fz, + $this->fn, + $this->ff, + $this->fp, + ); + + case 'tftn': + return sprintf('%d:%d/%d.%d', + $this->tz, + $this->tn, + $this->tf, + $this->tp, + ); + + default: + throw new \Exception('Unknown key: '.$key); + } + } + /** * Determine if a line is a kludge line. * @@ -20,12 +46,18 @@ abstract class FTN /** * This function creates our unpack header + * + * @param array $pack * @return string */ - protected function unpackheader(array $pack) + protected function unpackheader(array $pack): string { - return join('/',array_values(collect($pack) - ->sortBy(function($k,$v) {return $k[0];}) - ->transform(function($k,$v) {return $k[1].$v;})->toArray())); + return join('/', + collect($pack) + ->sortBy(function($k,$v) {return $k[0];}) + ->transform(function($k,$v) {return $k[1].$v;}) + ->values() + ->toArray() + ); } } \ No newline at end of file diff --git a/app/Classes/FTN/InvalidPacketException.php b/app/Classes/FTN/InvalidPacketException.php new file mode 100644 index 0000000..a27f600 --- /dev/null +++ b/app/Classes/FTN/InvalidPacketException.php @@ -0,0 +1,10 @@ + '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 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: "FMPT + 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. + * + * "INTL "" " + */ + 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: TOPT + elseif ($t = $this->kludge('TOPT ',$v)) + $this->_other->push($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 + * + * @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; + } +} \ No newline at end of file diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php new file mode 100644 index 0000000..fa9abed --- /dev/null +++ b/app/Classes/FTN/Packet.php @@ -0,0 +1,308 @@ + [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 = ''; + } + } +} \ No newline at end of file diff --git a/app/Classes/FTNMessage.php b/app/Classes/FTNMessage.php deleted file mode 100644 index 8d205ce..0000000 --- a/app/Classes/FTNMessage.php +++ /dev/null @@ -1,453 +0,0 @@ - '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 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: "FMPT - 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. - * - * "INTL "" " - */ - 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: TOPT - elseif ($t = $this->kludge('TOPT ',$v)) - $this->_other->push($t); - - // Time Zone of the sender. - elseif ($t = $this->kludge('TZUTC: ',$v)) - $this->tzutc= $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")); - - //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]); - } - } - } -} \ No newline at end of file diff --git a/app/Classes/FTNPacket.php b/app/Classes/FTNPacket.php deleted file mode 100644 index cc90d82..0000000 --- a/app/Classes/FTNPacket.php +++ /dev/null @@ -1,302 +0,0 @@ -[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 - } -} \ No newline at end of file diff --git a/app/Exceptions/InvalidFidoPacketException.php b/app/Exceptions/InvalidFidoPacketException.php deleted file mode 100644 index 3e4a327..0000000 --- a/app/Exceptions/InvalidFidoPacketException.php +++ /dev/null @@ -1,10 +0,0 @@ -with('user',Auth::user()); } + public function pkt(Request $request) + { + $pkt = NULL; + $file = NULL; + + if ($request->post()) { + $request->validate([ + 'file' => 'required|filled|min:1', + ]); + + foreach ($request->allFiles() as $key => $filegroup) { + if ($key !== 'file') + continue; + + foreach ($filegroup as $file) { + $pkt = new Packet($file); + break; + } + } + } + + return view('pkt.debug') + ->with('file',$file) + ->with('result',$pkt); + } + public function search(Request $request): Collection { $this->middleware('auth'); diff --git a/app/Models/Address.php b/app/Models/Address.php index e27d80f..f8cb726 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -86,13 +86,44 @@ class Address extends Model * Find a record in the DB for a node string, eg: 10:1/1.0 * * @param string $ftn - * @return Node|null + * @return Address|null * @throws Exception */ public static function findFTN(string $ftn): ?self { - $matches = []; + $ftn = self::parseFTN($ftn); + $o = (new self)->active() + ->select('addresses.*') + ->where('zones.zone_id',$ftn['z']) + ->where('host_id',$ftn['h']) + ->join('zones',['zones.id'=>'addresses.zone_id']) + ->join('domains',['domains.id'=>'zones.domain_id']) + ->where('zones.active',TRUE) + ->where('domains.active',TRUE) + ->where('addresses.active',TRUE) + ->where('node_id',$ftn['f']) + ->where('point_id',$ftn['p']) + ->when($ftn['d'],function($query,$domain) { + $query->where('domains.name',$domain); + }) + ->when((! $ftn['d']),function($query) { + $query->where('domains.default',TRUE); + }) + ->single(); + + return ($o && $o->system->active) ? $o : NULL; + } + + /** + * Parse a string and split it out as an FTN array + * + * @param string $ftn + * @return array + * @throws Exception + */ + public static function parseFTN(string $ftn): array + { // http://ftsc.org/docs/frl-1028.002 if (! preg_match('#^([0-9]+):([0-9]+)/([0-9]+)(.([0-9]+))?(@([a-z0-9\-_~]{0,8}))?$#',strtolower($ftn),$matches)) throw new Exception('Invalid FTN: '.$ftn); @@ -102,29 +133,17 @@ class Address extends Model if (! $matches[$i] || ($matches[$i] > DomainController::NUMBER_MAX)) throw new Exception('Invalid FTN: '.$ftn); } + if (isset($matches[5]) AND $matches[5] > DomainController::NUMBER_MAX) throw new Exception('Invalid FTN: '.$ftn); - $o = (new self)->active() - ->select('addresses.*') - ->where('zones.zone_id',$matches[1]) - ->where('host_id',$matches[2]) - ->join('zones',['zones.id'=>'addresses.zone_id']) - ->join('domains',['domains.id'=>'zones.domain_id']) - ->where('zones.active',TRUE) - ->where('domains.active',TRUE) - ->where('addresses.active',TRUE) - ->where('node_id',$matches[3]) - ->where('point_id',(isset($matches[5]) AND $matches[5]) ? $matches[5] : 0) - ->when(isset($matches[7]),function($query) use ($matches) { - $query->where('domains.name',$matches[7]); - }) - ->when((! isset($matches[7]) OR ! $matches[7]),function($query) { - $query->where('domains.default',TRUE); - }) - ->single(); - - return ($o && $o->system->active) ? $o : NULL; + return [ + 'z'=>(int)$matches[1], + 'n'=>(int)$matches[2], + 'f'=>(int)$matches[3], + 'p'=>isset($matches[5]) && $matches[5] ? (int)$matches[5] : 0, + 'd'=>$matches[7] ?? NULL + ]; } /** diff --git a/app/Models/Software.php b/app/Models/Software.php index 512d962..2a3e437 100644 --- a/app/Models/Software.php +++ b/app/Models/Software.php @@ -4,6 +4,24 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use App\Traits\ScopeActive; + class Software extends Model { + use ScopeActive; + + public const SOFTWARE_MAILER = 0x01; + public const SOFTWARE_TOSSER = 0x02; + + /* ATTRIBUTES */ + + public function getCodeAttribute($value) + { + return sprintf('%04X',$value); + } + + public function getNameAttribute($value) + { + return $value ?: 'Unknown'; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 417fc39..b560169 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -46,5 +46,17 @@ class AppServiceProvider extends ServiceProvider return NULL; }); + + // When a query should return 1 object, or NULL if it doesnt + Builder::macro('singleOrNew',function ($args) { + //dd(['func'=>func_get_args(),'args'=>$args,'this'=>$this]); + $result = $this->where($args)->get(); + + if ($result->count() == 1) { + return $result->first(); + } + + return $this->newModelInstance($args); + }); } } diff --git a/database/migrations/2021_06_28_125054_add_code_to_software.php b/database/migrations/2021_06_28_125054_add_code_to_software.php new file mode 100644 index 0000000..b64b1df --- /dev/null +++ b/database/migrations/2021_06_28_125054_add_code_to_software.php @@ -0,0 +1,37 @@ +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'); + }); + } +} diff --git a/public/oldschool/css/main.css b/public/oldschool/css/main.css index a0c2bd8..6d70a1a 100644 --- a/public/oldschool/css/main.css +++ b/public/oldschool/css/main.css @@ -201,6 +201,7 @@ div#search_results ul { color:#eeeeee; background-color:#292929; font-size: .85rem; + top:inherit !important; } div#search_results ul li.dropdown-header { display: block; diff --git a/resources/views/layouts/partials/sidebar.blade.php b/resources/views/layouts/partials/sidebar.blade.php index 77bb5e5..4287ec9 100644 --- a/resources/views/layouts/partials/sidebar.blade.php +++ b/resources/views/layouts/partials/sidebar.blade.php @@ -22,4 +22,9 @@
{{ $o->name }}
@endforeach + +
+
Debug
+
Verify Packet
+
\ No newline at end of file diff --git a/resources/views/pkt/debug.blade.php b/resources/views/pkt/debug.blade.php new file mode 100644 index 0000000..1b85ea9 --- /dev/null +++ b/resources/views/pkt/debug.blade.php @@ -0,0 +1,123 @@ +@extends('layouts.app') + +@section('content') +
+ @csrf + +
+
+
+

Upload Packet for Analysis

+

This packet will NOT be processed.

+ +
+
+ +
+ + + @error('file') + {{ $message }} + @else + A file is required. + @enderror + +
+
+
+ +
+
+ Cancel + +
+
+
+
+
+
+ + @if($result) +

Packet Results

+

Packet {{ $file->getClientOriginalName() }} (type {{ $result->type }}) is from {{ $result->fftn }} to {{ $result->tftn }}, dated {{ $result->date }}.

+

This packet has {{ $result->messages->count() }} messages and {{ $result->password ? 'DOES' : 'does NOT' }} have a password.

+

Tosser: {{ $result->software->code }} ({{ $result->software->name }}), version {{ $result->software_ver }}. Capabilities: {{ $result->capability }}.

+ @if ($result->messages->count() > 1) +

You can expand each one

+ @endif +
+
+ @foreach ($result->messages as $msg) +
+
+ + +
+
+ @if ($msg->errors) + @foreach ($msg->errors->messages()->all() as $error) +
+ {{ $error }} +
+ @endforeach + @endif + +
+
+ DATE: {{ $msg->date }} +
+
+ +
+
+ FROM: {{ $msg->user_from }} ({{ $msg->fftn }}) +
+
+ TO: {{ $msg->user_to }} ({{ $msg->tftn }}) +
+
+ +
+
+ SUBJECT: {{ $msg->subject }} +
+
+ +
+
+
+
{{ $msg->message }}
+
+
+
+ +
+
+ SEENBY:
{!! join('
',$msg->seenby->toArray()) !!} +
+
+ +
+
+ PATH:
{!! join('
',$msg->path->toArray()) !!} +
+
+ +
+
+ KLUDGES:
+ @foreach ($msg->kludge as $k => $v) + {{ $k }} {{ $v }}
+ @endforeach +
+
+
+
+
+
+ @endforeach +
+ @endif +@endsection \ No newline at end of file diff --git a/resources/views/system/addedit.blade.php b/resources/views/system/addedit.blade.php index 1f77bfa..52851f7 100644 --- a/resources/views/system/addedit.blade.php +++ b/resources/views/system/addedit.blade.php @@ -231,7 +231,7 @@
-

FidoNet system are also assigned some roles, and in some cases, those roles have a paritcular address format:

+

FidoNet system are also assigned some roles, and in some cases, those roles have a particular address format:

diff --git a/routes/web.php b/routes/web.php index ca42773..bada59e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -52,6 +52,7 @@ Route::middleware(['verified','activeuser'])->group(function () { Route::get('network/{o}',[HomeController::class,'network']); Route::get('permissions',[HomeController::class,'permissions']); +Route::match(['get','post'],'pkt',[HomeController::class,'pkt']); Route::get('search',[HomeController::class,'search']); Route::middleware(['auth','can:admin'])->group(function () {