93 lines
3.5 KiB
PHP
93 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
/**
|
|
* Which addresses genuinely belong to this installation.
|
|
*
|
|
* Derived, never typed. A hard-coded list is the same mistake the price sheet
|
|
* made when it promised "Adresse auf clupilot.cloud" while provisioning issued
|
|
* something else — except here the consequence is a customer being told that a
|
|
* phishing domain is one of ours, or that one of ours is not.
|
|
*
|
|
* Two registrable domains by design: the company's, which serves the website,
|
|
* the portal and the console, and the instance zone, which serves customer
|
|
* workloads. Keeping them apart is the point — a Nextcloud is third-party
|
|
* software that strangers sign in to, and on the same registrable domain as
|
|
* the portal it would share cookie scope with it.
|
|
*/
|
|
class OfficialDomains
|
|
{
|
|
/**
|
|
* The hostnames a customer legitimately sees, in the order they meet them.
|
|
*
|
|
* @return array<int, array{host: string, what: string}>
|
|
*/
|
|
public static function hosts(): array
|
|
{
|
|
$entries = [];
|
|
|
|
foreach ((array) config('admin_access.site_hosts', []) as $host) {
|
|
$entries[] = ['host' => (string) $host, 'what' => 'site'];
|
|
}
|
|
|
|
if ($app = (string) config('admin_access.app_host')) {
|
|
$entries[] = ['host' => $app, 'what' => 'portal'];
|
|
}
|
|
|
|
if ($status = (string) config('admin_access.status_host')) {
|
|
$entries[] = ['host' => $status, 'what' => 'status'];
|
|
}
|
|
|
|
// The instance zone as a pattern, because the label is per customer and
|
|
// naming one customer's address on a public page names that customer.
|
|
if ($zone = ProvisioningSettings::dnsZone()) {
|
|
$entries[] = ['host' => '*.'.$zone, 'what' => 'instance'];
|
|
}
|
|
|
|
// The console is deliberately absent. It is not an address a customer
|
|
// has any business visiting, and printing it on a public page tells an
|
|
// attacker where the operator interface lives.
|
|
return array_values(array_filter($entries, fn (array $e) => $e['host'] !== ''));
|
|
}
|
|
|
|
/**
|
|
* The registrable domains behind those hosts — what somebody actually has
|
|
* to check in the address bar.
|
|
*
|
|
* "Everything before the first slash ends in one of these" is a rule a
|
|
* person can apply under time pressure; a list of eight hostnames is not.
|
|
*
|
|
* Keyed by domain, valued by what it is for — so the page never has to
|
|
* work that out from the order it happens to come back in.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public static function registrable(): array
|
|
{
|
|
$domains = [];
|
|
|
|
foreach (self::hosts() as $entry) {
|
|
$labels = explode('.', ltrim($entry['host'], '*.'));
|
|
|
|
// Last two labels. Not a public-suffix implementation: this runs
|
|
// against an installation's own configured domains, which are known
|
|
// quantities, and pulling in a suffix list to be right about
|
|
// co.uk on a page that says "check the address bar" is not the
|
|
// trade this needs.
|
|
if (count($labels) < 2) {
|
|
continue;
|
|
}
|
|
|
|
$domain = implode('.', array_slice($labels, -2));
|
|
|
|
// First writer wins: the site is listed before the instance zone,
|
|
// so a single-domain installation is described as the company's
|
|
// rather than as the customers'.
|
|
$domains[$domain] ??= $entry['what'] === 'instance' ? 'instances' : 'company';
|
|
}
|
|
|
|
return $domains;
|
|
}
|
|
}
|