325 lines
7.5 KiB
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]',__METHOD__,$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]',__METHOD__,$value->ftn));
|
|
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;
|
|
}
|
|
} |