clrghouz/app/Classes/Node.php

325 lines
7.5 KiB
PHP

<?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 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 have already authed, we'll use that password.
if ($this->ftns_authed->count())
return $this->ftns_authed->first()->session('sespass');
else
return ($this->ftns->count() && ($x=$this->ftns->first()->session('sespass'))) ? $x : '-';
// Return how long our session has been connected
case 'session_time':
return Carbon::now()->diffInSeconds($this->start_time);
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) OR ! $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) {
if (! $sespass=$o->session('sespass'))
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->ftns_authed->push($o);
}
/**
* 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;
if ($this->ftns_authed->count() !== 1 || ! $this->ftns->count())
return FALSE;
$ftn = $this->ftns_authed->first()->ftn;
return $this->ftns->search(function($item) use ($ftn) {
if ($item->ftn === $ftn) {
$item->system->last_session = Carbon::now();
$item->system->save();
$this->authed = TRUE;
return TRUE;
}
}) !== FALSE;
}
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;
}
}