From 1fa566b26c47819b4711911a52104c20f433ba29 Mon Sep 17 00:00:00 2001 From: Deon George Date: Sat, 17 Jul 2021 15:48:07 +1000 Subject: [PATCH] Transfering netmail via EMSI --- app/Classes/FTN/Message.php | 6 +-- app/Classes/FTN/Packet.php | 19 +++++--- app/Classes/FTN/Process.php | 8 ++-- app/Classes/FTN/Process/Ping.php | 11 ++--- app/Classes/File/Mail.php | 38 ++++++++++++++++ app/Classes/File/Send.php | 75 ++++++++++++++++++++++++++----- app/Classes/Node.php | 18 ++++++-- app/Classes/Protocol.php | 8 ---- app/Classes/Protocol/EMSI.php | 24 +++++----- app/Classes/Protocol/Zmodem.php | 40 +++++++++-------- app/Classes/Sock/SocketClient.php | 19 +++++--- app/Console/Commands/EMSISend.php | 2 +- app/Models/Address.php | 21 ++++++--- app/Models/Netmail.php | 8 +++- app/Models/Setup.php | 12 +++++ 15 files changed, 226 insertions(+), 83 deletions(-) create mode 100644 app/Classes/File/Mail.php diff --git a/app/Classes/FTN/Message.php b/app/Classes/FTN/Message.php index 0ce0931..927e249 100644 --- a/app/Classes/FTN/Message.php +++ b/app/Classes/FTN/Message.php @@ -195,7 +195,7 @@ class Message extends FTNBase if (($x=$o->validate($domain))->fails()) { Log::debug('Message fails validation',['result'=>$x->errors()]); - throw new \Exception('Message validation fails:'.join(' ',$x->errors()->all())); + //throw new \Exception('Message validation fails:'.join(' ',$x->errors()->all())); } return $o; @@ -249,7 +249,7 @@ class Message extends FTNBase case 'date': 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)); + sprintf('%s %s',chop(Arr::get($this->header,$key)),(is_null($x=$this->kludge->get('tzutc')) || ($x < 0)) ? $x : '+'.$x)); case 'flags': case 'cost': return Arr::get($this->header,$key); @@ -404,7 +404,7 @@ class Message extends FTNBase foreach ($this->_kludge as $k=>$v) { if ($x=$this->kludge->get($k)) - $return .= sprintf("\01%s %s\r",$v,$x); + $return .= sprintf("\01%s%s\r",$v,$x); } $return .= $this->message."\r"; diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index 24e6890..3ff8809 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -230,12 +230,17 @@ class Packet extends FTNBase */ public function __toString(): string { - $return = $this->createHeader(); + // Cache the packet creation + static $return = NULL; - foreach ($this->messages as $o) - $return .= "\02\00".(string)$o; + if (is_null($return)) { + $return = $this->createHeader(); - $return .= "\00\00"; + foreach ($this->messages as $o) + $return .= "\02\00".(string)$o; + + $return .= "\00\00"; + } return $return; } @@ -296,7 +301,7 @@ class Packet extends FTNBase /** * When creating a new packet, set the header. * - * @param array $header + * @param Address $o */ private function newHeader(Address $o): void { @@ -329,8 +334,8 @@ class Packet extends FTNBase * Parse a message in a mail packet * * @param string $message - * @param Domain $domain - * @throws \Exception + * @param Domain|null $domain + * @throws InvalidPacketException */ public function parseMessage(string $message,Domain $domain=NULL): void { diff --git a/app/Classes/FTN/Process.php b/app/Classes/FTN/Process.php index 43b92b7..3f943b5 100644 --- a/app/Classes/FTN/Process.php +++ b/app/Classes/FTN/Process.php @@ -21,7 +21,7 @@ abstract class Process */ protected static function format_msg(string $text): string { - $msg = utf8_decode(join("\n",static::msg_header()))."\n"; + $msg = utf8_decode(join("\r",static::msg_header()))."\r"; $c = 0; $offset = 0; @@ -35,7 +35,7 @@ abstract class Process } // Look for a return - $return = strpos($text,"\n",$offset); + $return = strpos($text,"\r",$offset); if ($return !== FALSE) $return -= $offset; @@ -55,13 +55,13 @@ abstract class Process $subtext = substr($text,$offset,$space); } - $msg .= $ll.$subtext."\n"; + $msg .= $ll.$subtext."\r"; $offset += strlen($subtext)+1; } // In case our text is shorter than the loo for ($c; $cuser_to) !== 'ping') return FALSE; - $reply = sprintf("Your ping was received here on %s and it took %s to get here.\n", + $reply = sprintf("Your ping was received here on %s and it took %s to get here.\r", 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"; + $reply .= "\r"; + $reply .= "\r"; + $reply .= "Your message travelled along this path on the way here:\r"; foreach ($msg->via as $path) - $reply .= sprintf(" * %s\n",$path); + $reply .= sprintf(" * %s\r",$path); - $o = new Netmail(); + $o = new Netmail; $o->to = $msg->user_from; $o->from = Setup::PRODUCT_NAME; $o->subject = 'Ping Reply'; diff --git a/app/Classes/File/Mail.php b/app/Classes/File/Mail.php new file mode 100644 index 0000000..cab4492 --- /dev/null +++ b/app/Classes/File/Mail.php @@ -0,0 +1,38 @@ +action |= $action; + + switch ($action) { + case self::I_SEND: + $this->file = $mail; + $this->file_name = sprintf('%08X.PKT',Carbon::now()->timestamp); + $this->file_size = strlen($mail); + $this->file_mtime = Carbon::now()->timestamp; // @todo This timestamp should be consistent incase of retries + + break; + + default: + throw new \Exception('Unknown action: '.$action); + } + } + + public function read(int $start,int $length): string + { + return substr((string)$this->file,$start,$length); + } +} \ No newline at end of file diff --git a/app/Classes/File/Send.php b/app/Classes/File/Send.php index 317d601..ee79fec 100644 --- a/app/Classes/File/Send.php +++ b/app/Classes/File/Send.php @@ -8,6 +8,8 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use League\Flysystem\UnreadableFileException; +use App\Models\Address; + /** * Object representing the files we are sending * @@ -24,6 +26,7 @@ final class Send extends Item { private Collection $list; private ?Item $sending; + private Collection $packets; private mixed $f; // File descriptor private int $start; // Time we started sending @@ -33,6 +36,7 @@ final class Send extends Item { // Initialise our variables $this->list = collect(); + $this->packets = collect(); $this->sending = NULL; $this->file_pos = 0; $this->f = NULL; @@ -60,12 +64,14 @@ final class Send extends Item case 'mail_count': return $this->list ->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); }) - ->count(); + ->count() + + $this->packets->count(); case 'mail_size': return $this->list ->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); }) - ->sum(function($item) { return $item->file_size; }); + ->sum(function($item) { return $item->file_size; }) + + $this->packets->sum(function($item) { return $item->file_size; }); case 'sendas': return $this->sending ? $this->sending->{$key} : NULL; @@ -78,21 +84,31 @@ final class Send extends Item case 'total_sent': return $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) - ->count(); + ->count() + + $this->packets + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) + ->count(); case 'total_sent_bytes': return $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) - ->sum(function($item) { return $item->file_size; }); + ->sum(function($item) { return $item->file_size; }) + + $this->packets + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) + ->sum(function($item) { return $item->file_size; }); case 'total_count': return $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) - ->count(); + ->count() + + $this->packets + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) + ->count(); case 'total_size': return $this->list - ->sum(function($item) { return $item->file_size; }); + ->sum(function($item) { return $item->file_size; }) + + $this->packets->sum(function($item) { return $item->file_size; }); default: throw new Exception('Unknown key: '.$key); @@ -143,7 +159,9 @@ final class Send extends Item Log::debug(sprintf('%s: - Closing [%s], sent in [%d]',__METHOD__,$this->sending->file_name,$end)); } - fclose($this->f); + if (! $this->sending instanceof Mail) + fclose($this->f); + $this->sending = NULL; $this->file_pos = 0; $this->f = NULL; @@ -156,7 +174,7 @@ final class Send extends Item */ public function feof(): bool { - return feof($this->f); + return ($this->sending instanceof Mail) ? ($this->file_pos == $this->size) : feof($this->f); } /** @@ -169,6 +187,18 @@ final class Send extends Item { Log::debug(sprintf('%s: + Start',__METHOD__)); + // If we have mail, we'll send that first + if ($this->sending = $this->packets + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) + ->first()) + { + $this->file_pos = 0; + $this->start = time(); + $this->f = TRUE; + + return TRUE; + } + $this->sending = $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) ->first(); @@ -189,6 +219,19 @@ final class Send extends Item return TRUE; } + /** + * Add our mail to the send queue + * + * @param Address $ao + * @throws Exception + */ + public function mail(Address $ao): void + { + // Netmail + if ($x=$ao->getNetmail()) + $this->packets->push(new Mail($x,self::I_SEND)); + } + /** * Read bytes of the sending file * @@ -202,7 +245,13 @@ final class Send extends Item if (! $this->f) throw new Exception('No file open for read'); - $data = fread($this->f,$length); + // We are sending mail + if ($this->sending instanceof Mail) { + $data = $this->sending->read($this->file_pos,$length); + } else { + $data = fread($this->f,$length); + } + $this->file_pos += strlen($data); Log::debug(sprintf('%s: - Read [%d] bytes, file pos now [%d]',__METHOD__,strlen($data),$this->file_pos)); @@ -224,7 +273,13 @@ final class Send extends Item if (! $this->f) throw new Exception('No file open for seek'); - $rc = (fseek($this->f,$pos,SEEK_SET) === 0); + if ($this->sending instanceof Mail) { + $rc = ($pos < $this->size) ? $pos : $this->size; + + } else { + $rc = (fseek($this->f,$pos,SEEK_SET) === 0); + } + if ($rc) $this->file_pos = $pos; diff --git a/app/Classes/Node.php b/app/Classes/Node.php index e8adaab..6c67f8e 100644 --- a/app/Classes/Node.php +++ b/app/Classes/Node.php @@ -33,12 +33,14 @@ class Node private Collection $ftns; // The FTNs of the remote system private Collection $ftns_authed; // The FTNs we have validated + private bool $authed; // Have we authenticated the remote. private int $options; // This nodes capabilities/options public function __construct() { $this->options = 0; + $this->authed = FALSE; $this->start_time = Carbon::now(); $this->ftns = collect(); $this->ftns_authed = collect(); @@ -54,16 +56,20 @@ class Node case 'aka_num': return $this->ftns->count(); - // Number of AKAs we have validated + // The authenticated remote addresses + case 'aka_remote': + return $this->ftns_authed; + + // Have we authenticated the remote case 'aka_authed': - return $this->ftns_authed->count(); + return $this->authed; case 'ftn': return ($x=$this->ftns->first()) ? $x->ftn : 'Unknown'; // The nodes password case 'password': - return ($this->ftns_authed->count() && ($x=$this->ftns_authed->first()->session('sespass'))) ? $x : '-'; + return ($this->ftns->count() && ($x=$this->ftns->first()->session('sespass'))) ? $x : '-'; // Return how long our session has been connected case 'session_time': @@ -163,6 +169,7 @@ class Node $o->system->last_session = Carbon::now(); $o->system->save(); $this->ftns_authed->push($o); + $this->authed = TRUE; } } @@ -224,6 +231,10 @@ class Node */ public function originate_check(): bool { + // If we have already authed, we wont do it again + if ($this->authed) + return TRUE; + if ($this->ftns_authed->count() !== 1 || ! $this->ftns->count()) return FALSE; @@ -233,6 +244,7 @@ class Node if ($item->ftn == $ftn) { $item->system->last_session = Carbon::now(); $item->system->save(); + $this->authed = TRUE; return TRUE; } }) !== FALSE; diff --git a/app/Classes/Protocol.php b/app/Classes/Protocol.php index b233a71..cb4573f 100644 --- a/app/Classes/Protocol.php +++ b/app/Classes/Protocol.php @@ -12,11 +12,6 @@ use App\Models\{Address,Setup}; abstract class Protocol { - // Our product code - // @todo Move These to a config file - protected const product_code = 'AB8D'; - public const setup = 1; - // Enable extra debugging protected bool $DEBUG = FALSE; @@ -254,9 +249,6 @@ abstract class Protocol $this->client->speed = SocketClient::TCP_SPEED; $this->originate = FALSE; - // @todo While Debugging - $this->send->add('/tmp/aa'); - return $this->protocol_session(); default: diff --git a/app/Classes/Protocol/EMSI.php b/app/Classes/Protocol/EMSI.php index 5931feb..ee9dd37 100644 --- a/app/Classes/Protocol/EMSI.php +++ b/app/Classes/Protocol/EMSI.php @@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Log; use App\Classes\Protocol as BaseProtocol; use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketException; -use App\Models\Address; +use App\Models\{Address,Setup}; use App\Interfaces\CRC as CRCInterface; use App\Interfaces\Zmodem as ZmodemInterface; use App\Traits\CRC as CRCTrait; @@ -82,7 +82,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface if (! parent::onConnect($client)) { $this->session(self::SESSION_AUTO,$client,(new Address)); $this->client->close(); - Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->getAddress())); + Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->address_remote)); } return NULL; @@ -191,7 +191,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface // Mailer Details $makedata .= sprintf('{%s}{%s}{%s}{%s}', - self::product_code, + Setup::product_id(), config('app.name'), $this->setup->version, '#000000' // Serial Numbers @@ -346,7 +346,12 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface } else { $this->node->optionSet(self::O_PWD); Log::info(sprintf('%s: - Remote Authed [%d] AKAs',__METHOD__,$c)); - $this->send->add('/tmp/aa'); + + // Add our mail to the queue if we have authenticated + if ($this->node->aka_authed) + foreach ($this->node->aka_remote as $ao) { + $this->send->mail($ao); + } } /* Link codes */ @@ -961,9 +966,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface if ($rc < 0) return (self::S_REDIAL|self::S_ADDTRY); - Log::info(sprintf('%s: - Starting outbound EMSI session to [%s]',__METHOD__,$this->client->getAddress())); - - // @todo Lock Node AKAs + Log::info(sprintf('%s: - Starting outbound EMSI session to [%s]',__METHOD__,$this->client->address_remote)); // Inbound session } else { @@ -975,9 +978,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface return (self::S_REDIAL|self::S_ADDTRY); } - Log::info(sprintf('%s: - Starting inbound EMSI session from [%s]',__METHOD__,$this->client->getAddress())); - - // @todo Lock Node AKAs + Log::info(sprintf('%s: - Starting inbound EMSI session from [%s]',__METHOD__,$this->client->address_remote)); if ($this->node->aka_authed) { $xproto = $this->is_freq_available(); @@ -1003,6 +1004,8 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface return (self::S_REDIAL|self::S_ADDTRY); } + // @todo Lock Node AKAs + Log::info(sprintf('%s: - We have %lu%s mail, %lu%s files',__METHOD__,$this->send->mail_size,'b',$this->send->file_size,'b')); $proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK); @@ -1172,6 +1175,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->total_count) $z->zmodem_sendfile($this->send); + Log::debug(sprintf('%s: - Finished sending',__METHOD__)); return ($z->zmodem_senddone()<0); } } diff --git a/app/Classes/Protocol/Zmodem.php b/app/Classes/Protocol/Zmodem.php index 6dbf634..c834e8b 100644 --- a/app/Classes/Protocol/Zmodem.php +++ b/app/Classes/Protocol/Zmodem.php @@ -500,29 +500,31 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface { Log::debug(sprintf('%s: + Start',__METHOD__)); - try { - $send->open(); - $rc = $this->ls_zsendfile($send,$this->ls_SerialNum++,$send->total_count,$send->total_size); + while ($send->total_count && $send->open()) { + try { + $rc = $this->ls_zsendfile($send,$this->ls_SerialNum++,$send->total_count,$send->total_size); - switch ($rc) { - case self::LSZ_OK: - $send->close(TRUE); + switch ($rc) { + case self::LSZ_OK: + $send->close(TRUE); + break; - case self::ZSKIP: - case self::ZFERR: - $send->close(FALSE); - break; + case self::ZSKIP: + case self::ZFERR: + $send->close(FALSE); + break; - default: - $send->close(FALSE); - $this->ls_zabort(); - break; + default: + $send->close(FALSE); + $this->ls_zabort(); + break; + } + + return $rc; + + } catch (\Exception $e) { + Log::error(sprintf('%s: ! Error [%s]',__METHOD__,$e->getMessage())); } - - return $rc; - - } catch (\Exception $e) { - Log::error(sprintf('%s: ! Error [%s]',__METHOD__,$e->getMessage())); } return self::OK; diff --git a/app/Classes/Sock/SocketClient.php b/app/Classes/Sock/SocketClient.php index 9d23161..c15aeea 100644 --- a/app/Classes/Sock/SocketClient.php +++ b/app/Classes/Sock/SocketClient.php @@ -10,16 +10,18 @@ use Illuminate\Support\Str; * Class SocketClient * * @package App\Classes\Sock - * @property int speed * @property int cps + * @property int speed */ final class SocketClient { // For deep debugging private bool $DEBUG = FALSE; private \Socket $connection; - private string $address = ''; - private int $port = 0; + private string $address_local = ''; + private int $port_local = 0; + private string $address_remote = ''; + private int $port_remote = 0; // Our session state private array $session = []; @@ -52,14 +54,19 @@ final class SocketClient { private string $rx_buf = ''; public function __construct (\Socket $connection,int $speed=self::TCP_SPEED) { - socket_getsockname($connection,$this->address,$this->port); - Log::info(sprintf('%s: + Connection from [%s] on port [%d]',__METHOD__,$this->address,$this->port)); + socket_getsockname($connection,$this->address_local,$this->port_local); + socket_getpeername($connection,$this->address_remote,$this->port_remote); + Log::info(sprintf('%s: + Connection from [%s] on port [%d]',__METHOD__,$this->address_remote,$this->port_remote)); $this->connection = $connection; } public function __get($key) { switch ($key) { + case 'address_remote': + case 'port_remote': + return $this->{$key}; + case 'cps': case 'speed': return Arr::get($this->session,$key); @@ -244,6 +251,7 @@ final class SocketClient { * * @return string * @todo change to __get() + * @deprecated */ public function getAddress(): string { @@ -255,6 +263,7 @@ final class SocketClient { * * @return int * @todo change to __get() + * @deprecated */ public function getPort(): int { diff --git a/app/Console/Commands/EMSISend.php b/app/Console/Commands/EMSISend.php index 4280f85..710b6e3 100644 --- a/app/Console/Commands/EMSISend.php +++ b/app/Console/Commands/EMSISend.php @@ -51,6 +51,6 @@ class EMSISend extends Command $o = new EMSI(Setup::findOrFail(config('app.id'))); $o->session(EMSI::SESSION_AUTO,$client,$no); - Log::info(sprintf('Connection ended: %s',$client->getAddress()),['m'=>__METHOD__]); + Log::info(sprintf('Connection ended: %s',$client->address_remote),['m'=>__METHOD__]); } } diff --git a/app/Models/Address.php b/app/Models/Address.php index 59eb96d..7f52520 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -53,7 +53,8 @@ class Address extends Model return $this->hasMany(self::class,'hub_id','id'); case 'node': - return NULL; + // Nodes dont have children, but must return a relationship instance + return $this->hasOne(self::class,NULL,'void'); default: throw new Exception('Unknown role: '.$this->role); @@ -151,18 +152,24 @@ class Address extends Model /** * Get netmail for this node (including it's children) + * + * @return Packet|null */ - public function getNetmail(): Packet + public function getNetmail(): ?Packet { - $o = new Packet($this); + if (($x=Netmail::whereIn('tftn_id',$this->children->pluck('id')->push($this->id)))->count()) { + $o = new Packet($this); - foreach (Netmail::whereIn('tftn_id',$this->children->pluck('id')->push($this->id))->get() as $oo) { - $o->addNetmail($oo->packet()); + foreach ($x->get() as $oo) { + $o->addNetmail($oo->packet()); - // @todo We need to mark the netmail as sent + // @todo We need to mark the netmail as sent + } + + return $o; } - return $o; + return NULL; } /** diff --git a/app/Models/Netmail.php b/app/Models/Netmail.php index 5beabc3..b4fff3f 100644 --- a/app/Models/Netmail.php +++ b/app/Models/Netmail.php @@ -21,7 +21,8 @@ class Netmail extends Model { return $this ->setConnection('pgsql') - ->belongsTo(Address::class); + ->belongsTo(Address::class) + ->withTrashed(); } public function tftn() @@ -52,6 +53,8 @@ class Netmail extends Model { $o = new Message; + // @todo Dont bundle mail to nodes that have been disabled, or addresses that have been deleted + $o->header = [ 'onode' => $this->fftn->node_id, 'dnode' => $this->tftn->node_id, @@ -88,6 +91,9 @@ class Netmail extends Model // @todo Point handling FMPT/TOPT $o->intl = sprintf('%s %s',$this->tftn->ftn3d,$this->fftn->ftn3d); + // TZUTC + $o->kludge->put('tzutc',str_replace('+','',$this->created_at->getOffsetString(''))); + return $o; } } \ No newline at end of file diff --git a/app/Models/Setup.php b/app/Models/Setup.php index 77826c8..0b34249 100644 --- a/app/Models/Setup.php +++ b/app/Models/Setup.php @@ -42,9 +42,21 @@ class Setup extends Model public const PRODUCT_VERSION_MAJ = 0; public const PRODUCT_VERSION_MIN = 0; + public const hexdigitslower = '0123456789abcdef'; + // Our non model attributes and values private array $internal = []; + public static function product_id(int $c=self::PRODUCT_ID): string + { + $x = substr(static::hexdigitslower,($c&0xf000)>>12,1); + $x .= substr(static::hexdigitslower,($c&0x0f00)>>8,1); + $x .= substr(static::hexdigitslower,($c&0x00f0)>>4,1); + $x .= substr(static::hexdigitslower,($c&0x000f),1); + + return $x; + } + /* RELATIONS */ public function system()