clrghouz/app/helpers.php
Deon George b59317871a Added tool to list node addresses and the address we use,
Fix address edit which reactivated the address,
Fix correct icons when using address merge,
We only advertise validated addresses and use validated addresses for routing,
Show our address on known AKA screen
2025-04-15 09:32:50 +10:00

313 lines
8.2 KiB
PHP

<?php
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use App\Models\{Address,Domain,Setup};
/**
* Calculate CCITT-CRC16 checksum
*/
if (! function_exists('crc16')) {
function crc16($data): int
{
$crc = 0x0000;
for ($i = 0; $i < strlen($data); $i++) {
$x = (($crc >> 8) ^ ord($data[$i])) & 0xFF;
$x ^= $x >> 4;
$crc = (($crc << 8) ^ ($x << 12) ^ ($x << 5) ^ $x) & 0xFFFF;
}
return $crc;
}
}
/**
* Dump out data into a hex dump
*/
if (! function_exists('hex_dump')) {
function hex_dump($data,$newline="\n",$width=16): string
{
$result = '';
$pad = '.'; # padding for non-visible characters
$to = $from = '';
for ($i=0; $i<=0xFF; $i++) {
$from .= chr($i);
$to .= ($i >= 0x20 && $i <= 0x7E) ? chr($i) : $pad;
}
$hex = str_split(bin2hex($data),$width*2);
$chars = str_split(strtr($data,$from,$to),$width);
$offset = 0;
foreach ($hex as $i => $line) {
$result .= sprintf('%08X: %-48s [%s]%s',
$offset,
substr_replace(implode(' ',str_split($line,2)),' ',8*3,0),
$chars[$i],
$newline);
$offset += $width;
}
return $result;
}
}
/**
* Send a value has hex chars
*/
if (! function_exists('hexstr')) {
function hexstr(int $int): string
{
if ($int > 0xffff)
throw new Exception('Int too large for hexstr');
$hexdigitslower = '0123456789abcdef';
$x = '';
if ($int > 0xff) {
$x .= substr($hexdigitslower,($int&0xf000)>>12,1);
$x .= substr($hexdigitslower,($int&0x0f00)>>8,1);
}
$x .= substr($hexdigitslower,($int&0x00f0)>>4,1);
$x .= substr($hexdigitslower,($int&0x000f),1);
return $x;
}
}
/**
* Return our addresses.
*
* We have two address types:
* + a) address that we advertise to receive mail and use to package up mail (Public)
* + b) address that we accept and process mail
*
* a is a subset of b, where b might have addresses we used in the past but no longer (but mail still comes in, and we
* dont want to send out mail with those addresses anymore)
*
* Public addresses are when validated is set to true
* When determining our public addresses for a specific Address (when Address::class is passed), we only return addresses
* when security is not null
*
* If domain provided, limit the list to those within the domain - returning a Collection::class
* If address provided, return our address that would be used for the provided address - return Address::class
*
* @param Domain|Address|null $o Domain or Address
* @param bool $public Return only public addresses
* @return Collection|Address|NULL
* @throws Exception
*/
function our_address(Domain|Address $o=NULL,bool $public=TRUE): Collection|Address|NULL
{
if (! Config::has('setup'))
Config::set('setup',Setup::findOrFail(config('app.id')));
$so = Config::get('setup');
$so->loadMissing([
'system:id,name,sysop,location',
'system.akas:addresses.id,addresses.zone_id,region_id,host_id,node_id,point_id,addresses.system_id,addresses.active,role,validated,security',
'system.akas.zone:id,domain_id,zone_id',
'system.akas.zone.domain:id,name',
]);
// If we dont have any addresses
if ($so->system->akas->count() === 0)
return NULL;
// We havent asked for an address/domain, so we'll return them all.
if (is_null($o))
return $so->system->akas;
// We are requesting a list of addresses for a Domain, or a specific Address, and we have more than 1
switch (get_class($o)) {
// Looking for addresses in the same domain, and if fido.strict, addresses that have a higher role (ie: uplink)
case Address::class:
$filter = $so->system->akas
->filter(fn($item)=>((! $public) || ($public && $item->validated)) && ($item->zone->domain_id === $o->zone->domain_id))
->sortBy('role_id');
if (config('fido.strict') && ($x=$filter->filter(fn($item)=>$item->role_id <= $o->role_id)->sortBy('role_id'))->count())
$filter = $x;
return $filter->count()
? ($filter->filter(fn($item)=>$item->region_id === $o->region_id)->count()
? $filter->last()
: $filter->first())->unsetRelation('nodes_hub')
: NULL;
// Addresses in this domain
case Domain::class:
return $so->system->akas
->filter(fn($item)=>((! $public) || ($public && $item->validated)) && ($item->zone->domain_id === $o->id));
// We shouldnt get here
default:
throw new Exception('Unhandled class: '.get_class($o));
}
}
function our_hostname(Address $o): string
{
$our = our_address($o->domain)->first();
$ourhostname = $our->system->address;
switch ($our->role_id) {
case Address::NODE_ZC:
$domain = collect(explode('.',$ourhostname))->forget(0)
->prepend(sprintf('z%d',$our->zone->zone_id));
break;
case Address::NODE_RC:
case Address::NODE_NC:
$domain = collect(explode('.',$ourhostname))->forget(0)
->prepend(sprintf('z%d',$our->zone->zone_id))
->prepend(sprintf('n%d',$our->host_id));
break;
case Address::NODE_HC:
$domain = collect(explode('.',$ourhostname))->forget(0)
->prepend(sprintf('z%d',$our->zone->zone_id))
->prepend(sprintf('n%d',$our->host_id))
->prepend(sprintf('f%d',$our->node_id));
break;
default:
$domain = collect(explode('.',gethostname()));
}
return $domain->join('.');
}
/**
* Return a list of nodes that collect mail directly from us
*
* @param Domain|NULL $do
* @return Collection
*/
function our_nodes(Domain $do=NULL): Collection
{
return Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id','addresses.system_id','role'])
->join('system_zone',['system_zone.system_id'=>'addresses.system_id','system_zone.zone_id'=>'addresses.zone_id'])
->join('zones',['zones.id'=>'system_zone.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->when(! is_null($do),
fn($query)=>$query
->where('domain_id',$do->id))
->ActiveFTN()
->FTNorder()
->get();
}
if (! function_exists('timew')) {
/**
* Convert a time into an 32 bit value. This is primarily used to create 8 character hex filenames that
* are unique using 1/10th second precision.
*
* Time is:
* + 02 bits least significant bits of year - giving us a 4 year timestamp
* + 04 bits Month
* + 05 bits Day
* + 05 bits Hour
* + 06 bits Min
* + 06 bits Sec
* + 04 bits 10th Sec
* = 32 bits
*
* @param Carbon|null $time
* @return int
* @todo Since this is used as part of our msgid, we need to use the first 2 bits to get our 3 year unique msgid, ie: year&0x03
*/
function timew(Carbon $time=NULL): int
{
static $delay = 0;
// If we are not passed a time, we'll use the time now
if (! $time) {
// In case we are called twice, we'll delay 1/10th second so we have a unique result.
if (Carbon::now()->getPreciseTimestamp(1) === $delay)
usleep(100000);
$time = Carbon::now();
}
$delay = $time->getPreciseTimestamp(1);
$t = 0;
$t = ($t | $time->year & 0x3) << 4;
$t = ($t | ($time->month & 0xf)) << 5;
$t = ($t | ($time->day & 0x1f)) << 5;
$t = ($t | ($time->hour & 0x1f)) << 6;
$t = ($t | ($time->minute & 0x3f)) << 6;
$t = ($t | ($time->second & 0x3f)) << 4;
$t = ($t | ((int)($time->milli/100)) & 0xf);
return $t;
}
}
if (! function_exists('wtime')) {
/**
* Convert a 40 bit integer into a time.
* We need to filter out loose bits, ie:
* + year all bits valid
* + month 11xx and 0000 are invalid
* + day all bits valid except 0000
* + hour 11xxx are invalid
* + min 1111xx are invalid
* + sec 1111xx are invalid
* + 1/2 11xx, 101x are invalid
* @see timew()
*
* @param int $time
* @param int|null $year
* @return Carbon
*/
function wtime(int $time,int $year=NULL): Carbon
{
if (! $year)
$year = Carbon::now()->year;
// Does the time have milli seconds?
if ($time > pow(2,26)-1) {
$milli = ($time & 0xf);
$time = $time >> 4;
if ($milli > 9)
$milli = 9;
} else {
$milli = 0;
}
$sec = ($time & 0x3f);
if ($sec > 59)
$sec = 59;
$time = $time >> 6;
$min = ($time & 0x3f);
if ($min > 59)
$min = 59;
$time = $time >> 6;
$hr = ($time & 0x1f);
if ($hr > 23)
$hr = 23;
$time = $time >> 5;
$day = ($time & 0x1f);
$time = $time >> 5;
$month = ($time & 0xf);
if ($month > 12)
$month = 12;
$time = $time >> 4;
return Carbon::create(($year & 0xffc)+$time,$month,$day,$hr,$min,$sec+$milli/10);
}
}