<?php namespace App\Classes; use Carbon\Carbon; use Exception; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use App\Models\Address; /** * Object representing the node we are communicating with * * @property int aka_authed * @property int aka_num * @property string ftn * @property string password * @property Carbon node_time * @property int session_time * @property int ver_major * @property int ver_minor */ class Node { private const LOGKEY = 'N--'; // Remote Version private int $version_major = 0; private int $version_minor = 0; private Carbon $start_time; // The time our connection started // @todo Change this to Carbon private string $node_time; // Current node's time private string $node_timezone; // Current node's time zone private Collection $ftns; // The FTNs of the remote system private Collection $ftns_authed; // The FTNs we have validated private Collection $ftns_other; // Other FTN addresses presented private bool $authed; // Have we authenticated the remote. private Address $originate; // When we originate a call, this is who we are after 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(); $this->ftns_other = collect(); // @todo This should be configured in the DB for each FTN system $this->node_timezone = 'Australia/Melbourne'; } /** * @throws Exception */ public function __get($key) { switch ($key) { case 'address': return ($x=$this->ftns_authed)->count() ? $x->first() : $this->ftns->first(); // Number of AKAs the remote has case 'aka_num': return $this->ftns->count(); // The authenticated remote addresses case 'aka_remote_authed': return $this->ftns_authed; case 'aka_remote': return $this->ftns; // Have we authenticated the remote case 'aka_authed': return $this->authed; // FTNs that we dont know about case 'aka_other': return $this->ftns_other; // The nodes password case 'password': // If we are originating a session, we'll use that password. if (isset($this->originate)) return $this->originate->pass_session; // If we have already authed, we'll use that password. if ($this->ftns_authed->count()) return $this->ftns_authed->first()->pass_session; else return ($this->ftns->count() && ($x=$this->ftns->first()->pass_session)) ? $x : '-'; // Return how long our session has been connected case 'session_time': return sprintf("%d",$this->start_time->diffInSeconds(Carbon::now())); case 'system': case 'sysop': case 'location': case 'phone': case 'flags': case 'message': case 'files': case 'node_time': case 'node_timezone': case 'netmail': // The current session speed case 'speed': // The time our session started. case 'start_time': case 'software': return $this->{$key}; // Client version case 'ver_major': return $this->version_major; case 'ver_minor': return $this->version_minor; default: throw new Exception('Unknown key: '.$key); } } /** * @throws Exception */ public function __set($key,$value) { switch ($key) { case 'ftn': if ((! is_object($value)) || (! $value instanceof Address)) throw new Exception('Not an Address object: '.(is_object($value) ? get_class($value) : serialize($value))); // Ignore any duplicate FTNs that we get if ($this->ftns->search(function($item) use ($value) { return $item->id === $value->id; }) !== FALSE) { Log::debug(sprintf('%s:- Ignoring Duplicate FTN [%s]',self::LOGKEY,$value->ftn)); break; } $this->ftns->push($value); break; case 'ftn_other': // Ignore any duplicate FTNs that we get if ($this->ftns_other->search($value) !== FALSE) { Log::debug(sprintf('%s:- Ignoring Duplicate FTN [%s]',self::LOGKEY,$value)); break; } $this->ftns_other->push($value); break; case 'system': case 'sysop': case 'location': case 'phone': case 'flags': case 'message': case 'files': case 'netmail': case 'software': case 'speed': case 'start_time': case 'node_time': case 'node_timezone': case 'ver_major': case 'ver_minor': $this->{$key} = $value; break; default: throw new Exception('Unknown variable: '.$key); } } /** * Authenticate the AKAs that the node provided * * @param string $password * @param string $challenge * @return int * @throws Exception */ public function auth(string $password,string $challenge=''): int { Log::debug(sprintf('%s:+ auth [%s]',self::LOGKEY,$password)); $so = NULL; // Make sure we havent been here already if ($this->ftns_authed->count()) throw new Exception('Already authed'); foreach ($this->ftns as $o) { Log::debug(sprintf('%s:- Attempting to authenticate [%s] with [%s]',self::LOGKEY,$o->ftn,$o->pass_session)); if (! $sespass=$o->pass_session) continue; // If we have challenge, then we are doing MD5 $exp_pwd = $challenge ? $this->md5_challenge($sespass,$challenge) : $sespass; if ($exp_pwd === $password) { if (! $so) $so = $o->system; // Make sure we are the same system if ($so->id !== $o->system->id) { Log::alert(sprintf('%s:! AKA [%s] is from a different system?',self::LOGKEY,$o->ftn),['so'=>$so->name]); continue; } $this->ftns_authed->push($o); $this->authed = TRUE; } } if ($this->authed) { $o->system->last_session = Carbon::now(); $o->system->save(); } Log::debug(sprintf('%s:= auth, we authed [%d] addresses',self::LOGKEY,$this->ftns_authed->count())); return $this->ftns_authed->count(); } /** * When we originate a connection and need to send our MD5 Challenge response * * @param string $challenge * @return string * @throws Exception */ public function get_md5chal(string $challenge): string { return $this->md5_challenge($this->password,$challenge); } /** * Return the remotes BINKP version as a int * * @return int */ public function get_versionint(): int { return $this->ver_major*100+$this->ver_minor; } /** * Calculate the response to an MD5 challenge, using the nodes password * * @param $pwd * @param $challenge * @return string */ private function md5_challenge($pwd,$challenge): string { $x = $pwd.str_repeat(chr(0x00),64-strlen($pwd)); return md5($this->str_xor($x,0x5c).md5($this->str_xor($x,0x36).$challenge,true)); } /** * When we originate a call to a node, we need to store the node we are connecting with in the ftns_authed, so * authentication proceeds when we send our M_pwd * * @param Address $o */ public function originate(Address $o): void { $this->originate = $o; $this->ftns_authed = $o->system->match($o->zone,Address::NODE_ALL); } /** * Check that our received FTNs match who we called * * @return bool */ public function originate_check(): bool { // If we have already authed, we wont do it again if ($this->authed) return TRUE; Log::debug(sprintf('%s:- Making sure we called [%s] from [%s]',self::LOGKEY,$this->originate->ftn,$this->ftns->pluck('ftn')->join(','))); $this->authed = $this->ftns->pluck('ftn')->contains($this->originate->ftn); return $this->authed; } public function optionClear(int $key): void { $this->options &= ~$key; } public function optionGet(int $key): int { return ($this->options & $key); } public function optionSet(int $key): void { $this->options |= $key; } private function str_xor(string $string,int $val): string { $result = ''; for ($i=0;$i<strlen($string);$i++) $result .= chr(ord($string[$i]) ^ $val); return $result; } }