Full code refactor to Symfony7.4 with FrankenPHP

Includes significant breaking changes.
Default port and transportation changed to
http via port 80
Names of environment variables have changed.
This commit is contained in:
2025-12-15 12:30:14 -05:00
parent f5a5d63eb7
commit 102b9f3e78
50 changed files with 6228 additions and 826 deletions
+152
View File
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace App;
use BaconQrCode\Renderer\PlainTextRenderer;
use BaconQrCode\Writer;
use DateTimeImmutable;
use OTPHP\TOTP;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
class Utilities {
/* top-level-domains which are known to have multiple parts
* stored backwards TLD[uk][co] means ".co.uk" */
private const TLD = [
'ai' => ['com','net','off','org'],
'am' => ['radio'],
'com' => ['br','cn','co','de','eu','gr','it','jpn','mex','ru','sa','uk','us','za'],
'de' => ['com'],
'fm' => ['radio'],
'gg' => ['co','net','org'],
'in' => ['co','firm','gen','ind','net','org'],
'je' => ['co','net','org'],
'mx' => ['com','net','org'],
'net' => ['gb','hu','in','jp','se','uk'],
'nz' => ['co','net','org'],
'org' => ['ae','us'],
'ph' => ['com','net','org'],
'se' => ['com'],
'uk' => ['co','me','org'],
];
private const PATH_REGEX = '/[^A-Za-z0-9_.\/-]+/';
private const NO_UP_REGEX = '/\.{2,}/';
private const KEY_REGEX = '/[^A-Za-z0-9_.]+/';
public function __construct(
private readonly ClockInterface $clock,
private readonly CacheItemPoolInterface $appCache,
) {}
/**
* This lets us determine the base domain of the given ip, localhost, or domain
* "service.example.co.uk" into "example.co.uk" and "service.example.com" into "example.com"
* @param string $host ip, localhost, or domain with zero or more subdomains
* @param bool $strict return null if host is ip or localhost, default false
* @return ?string returns same ip, hostname, or domain with all subdomains removed
*/
public function baseDomain(string $host, bool $strict = false): ?string {
/* if host is an ip address (or localhost), leave it as is */
if (filter_var($host, FILTER_VALIDATE_IP) || $host === 'localhost') {
return $strict ? null : $host;
}
$parts = explode('.', $host);
$keep = $this->baseLength($parts);
$parts = array_slice($parts, -$keep);
return implode('.', $parts);
}
/**
* Build new domain name based on base of the given host and given subdomain
* Handles edge cases like host being an ip or localhost
* @param ?string $subdomain subdomain to use, allowed to be null
* @param string $host domain (optionally with subdomains), ip, or localhost
* @return string returns a valid hostname, typically "subdomain.baseDomain"
*/
public function buildDomain(?string $subdomain, string $host): string {
$base = $this->baseDomain($host, true);
if ($base === null) {
return $host;
}
return ($subdomain === null || $subdomain === '')
? $base : "$subdomain.$base";
}
private function baseLength(array $parts): int {
$length = count($parts);
$baseLength = min(2, $length);
/* check if host should retain 3 parts, due to TLD */
if (count($parts) > 2 && isset(self::TLD[$parts[$length-1]]) &&
in_array($parts[$length-2], self::TLD[$parts[$length-1]], true)
) {
$baseLength = min(3, $length);
}
return $baseLength;
}
public function makeCacheKey(string $name): string {
return preg_replace(self::KEY_REGEX, '_', $name);
}
public function cleanPath(string $path): string {
return preg_replace([self::PATH_REGEX, self::NO_UP_REGEX], ['', '.'], $path);
}
public function hash(ParameterBag $query): string {
/* remember this is *NOT* cryptographically secure */
$array = $query->all();
ksort($array);
return hash('xxh3', json_encode($array));
}
/** @throws InvalidArgumentException we sanitize cache keys, to prevent this */
public function loadTotp(): string {
/* user forgot to set their TOTP_URI in the environment */
if ($this->appCache->hasItem('totp')) {
return $this->appCache->getItem('totp')->get();
}
return $this->makeTotp();
}
/** @throws InvalidArgumentException we sanitize cache keys, to prevent this */
private function makeTotp(): string {
/* we have not stored a totp into the app cache yet */
$totpObj = TOTP::generate($this->clock);
$totpObj->setLabel('Preauth-TOTP');
$totp = $totpObj->getProvisioningUri();
$totpItem = $this->appCache->getItem('totp');
$totpItem->set($totp);
/* per PSR6, if no expiration is set, implementation may set a default,
* we want this to keep forever, so a few hundred years should do it */
$totpItem->expiresAt(DateTimeImmutable::createFromFormat(
'Y-m-d', '2999-12-31'
));
$this->appCache->save($totpItem);
$this->showTotp($totp);
return $totp;
}
/** @throws InvalidArgumentException we sanitize cache keys, to prevent this */
private function showTotp(string $totp): void {
/* only show this at most, every 5 minutes */
$suppress = $this->appCache->getItem('suppress');
if ( ! $suppress->isHit()) {
$writer = new Writer(new PlainTextRenderer());
file_put_contents(
'php://stderr', <<<RAW
{$writer->writeString($totp)}
$totp
loading totp, because the env is not set, please copy above into TOTP_URI
RAW, FILE_APPEND
);
$suppress->expiresAfter(300);
$this->appCache->save($suppress);
}
}
}