Add Zmodem/BINKP/EMSI
This commit is contained in:
260
app/Classes/Node.php
Normal file
260
app/Classes/Node.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Models\Node as NodeModel;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// 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 Collection $ftns; // The FTNs of the remote system
|
||||
private Collection $ftns_authed; // The FTNs we have validated
|
||||
|
||||
private int $options; // This nodes capabilities/options
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->options = 0;
|
||||
$this->start_time = Carbon::now();
|
||||
$this->ftns = collect();
|
||||
$this->ftns_authed = collect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
// Number of AKAs the remote has
|
||||
case 'aka_num':
|
||||
return $this->ftns->count();
|
||||
|
||||
// Number of AKAs we have validated
|
||||
case 'aka_authed':
|
||||
return $this->ftns_authed->count();
|
||||
|
||||
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()->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 '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 NodeModel)
|
||||
throw new Exception('Not a node 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 '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 '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: + Start [%s]',__METHOD__,$password));
|
||||
|
||||
// Make sure we havent been here already
|
||||
if ($this->ftns_authed->count())
|
||||
throw new Exception('Already authed');
|
||||
|
||||
foreach ($this->ftns as $o) {
|
||||
if (! $o->sespass)
|
||||
continue;
|
||||
|
||||
// If we have challenge, then we are doing MD5
|
||||
$exp_pwd = $challenge ? $this->md5_challenge($o->sespass,$challenge) : $o->sespass;
|
||||
|
||||
if ($exp_pwd === $password)
|
||||
$this->ftns_authed->push($o);
|
||||
}
|
||||
|
||||
Log::debug(sprintf('%s: = End [%d]',__METHOD__,$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 NodeModel $o
|
||||
*/
|
||||
public function originate(NodeModel $o): void
|
||||
{
|
||||
$this->ftns->push($o);
|
||||
$this->ftns_authed->push($o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that our received FTNs match who we called
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function originate_check(): bool
|
||||
{
|
||||
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) {
|
||||
return $item->ftn == $ftn;
|
||||
}) !== 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user