ldc-web-admin-secure-laravel/src/AdminSecure.php

250 lines
8 KiB
PHP
Raw Normal View History

<?php
namespace LDC\WebAdminSecureLaravel;
use Dxw\CIDR\IPAddress;
use Dxw\CIDR\IPv4Address;
use Dxw\CIDR\IPv4Range;
use Dxw\CIDR\IPv6Address;
use Dxw\CIDR\IPv6Range;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use LDC\WebAdminSecureLaravel\Exceptions\AdminSecureException;
use LDC\WebAdminSecureLaravel\Exceptions\AdminSecureInvalidAdressException;
abstract class AdminSecure
{
/**
* The cache key where to store ip list.
*
* @var string
*/
private static string $cache_key = 'ldcweb_ip_list';
/**
* The cache key where to store config cidr list.
*
* @var string
*/
private static string $cache_config_key = 'ldcweb_ip_cidr_config_list';
/**
* Private ipV4 CIDR list
*
* @var array
*/
private static array $private_ip_v4_cidr_list = [
'10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
];
/**
* Private ipV4 CIDR loopback
*
* @var string
*/
private static string $private_ip_v4_loopback_cidr = '127.0.0.0/8';
/**
* Private ipV6 CIDR list
*
* @var array
*/
private static array $private_ip_v6_cidr_list = [
'fd00::/8'
];
/**
* Private ipV6 CIDR loopback
*
* @var string
*/
private static string $private_ip_v6_loopback_cidr = '::1/128';
/**
* Check if ip is private
*
* @param \Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address $ip_address
* @return boolean
*/
public static function ipIsPrivate(IPv4Address|IPv6Address $ip_address): bool
{
return \get_class($ip_address) === IPv4Address::class ?
self::ipV4IsPrivate($ip_address) : self::ipV6IsPrivate($ip_address);
}
/**
* Fetch client IP
*
* @return \Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If the ip is invalid.
*/
public static function fetchClientIp(): IPv4Address|IPv6Address
{
// ! HA PROXY HACK.
$ip = request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
$ip = IPAddress::Make($ip);
if ($ip->isErr()) {
throw new AdminSecureException(\trans('Client IP not valid ') . $ip->getErr());
}
return $ip->unwrap();
}
/**
* Fetch Ip Adress list from LDC Web
*
* @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If Wrong env value.
* @throws \Illuminate\Http\Client\RequestException If calling admin secure fails.
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
public static function fetchIpList(): array
{
// phpcs:enable
$endpoint = config('admin-secure.endpoint');
$key = config('admin-secure.apiKey');
if (!$endpoint or !\filter_var($endpoint, \FILTER_VALIDATE_URL)) {
throw new AdminSecureException('LDC_WEB_API_ENDPOINT is required in .env to ba a valid url.');
}
if (!$key) {
throw new AdminSecureException('LDC_WEB_API_KEY is required in .env .');
}
$response = Http::get($endpoint, ['token' => $key])->throw();
$adresses = self::parseAdresses($response);
Log::info(\sprintf('ADMINSECURE : Fetched %d ip(s) from LDC WEB', \count($adresses)));
return $adresses;
}
/**
* Fetch Ip Adress list from LDC Web
*
* @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If Wrong env value.
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
public static function fetchConfigAllowedCidrList(): array
{
$allowed_cidr_config_list = \collect(config('admin-secure.cidrManualAllowedList'))->filter();
$allowed_cidr_config_list_hash = \md5($allowed_cidr_config_list->implode(','));
$allowed_cidr_list = $allowed_cidr_config_list
->map(function (string $cidr) {
$ip_range = !IPv4Range::Make($cidr)->isErr() ? IPv4Range::Make($cidr) : IPv6Range::Make($cidr);
if ($ip_range->isErr()) {
throw new AdminSecureException(\trans(
'LDC_WEB_CIDR_MANUAL_ALLOWED_LIST not valid (ipv4 or ipv6 cidr) :cidr',
[
'cidr' => $cidr
]
));
}
return $ip_range->unwrap();
})->all();
// * Flush cache if hash config hash changed
if ($allowed_cidr_config_list_hash !== \collect(Cache::get(self::$cache_config_key))->get('hash')) {
Cache::flush(self::$cache_config_key);
}
/** @var array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> $allowed_cidr_list */
return \collect(Cache::rememberForever(self::$cache_config_key, fn () => [
'list' => $allowed_cidr_list,
'hash' => $allowed_cidr_config_list_hash
]))->get('list');
}
/**
* Get ip list from cache
*
* @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
*/
public static function getCacheList(): array
{
try {
$ip_list = Cache::get(self::$cache_key, self::fetchIpList());
self::validateIpList($ip_list);
/** @var array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address> $ip_list */
return $ip_list;
} catch (AdminSecureInvalidAdressException) {
$ip_list = self::fetchIpList();
self::validateIpList($ip_list);
self::setCacheList($ip_list);
return $ip_list;
}
}
/**
* Set ip list to cache
*
* @param array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address> $ip_list
* @return void
*/
public static function setCacheList(array $ip_list): void
{
self::validateIpList($ip_list);
Cache::put(self::$cache_key, $ip_list);
}
/**
* Check if ipV4 is private
*
* @param \Dxw\CIDR\IPv4Address $ipv4_address
* @return boolean
*/
private static function ipV4IsPrivate(IPv4Address $ipv4_address): bool
{
return \collect(self::$private_ip_v4_cidr_list)
->concat([self::$private_ip_v4_loopback_cidr])
->map(fn (string $ipv4_cidr) => IPv4Range::Make($ipv4_cidr)->unwrap())
->reduce(fn (bool|null $initial, IPv4Range $ipv4_range) =>
$initial || $ipv4_range->containsAddress($ipv4_address));
}
/**
* Check if ipV6 is private
*
* @param \Dxw\CIDR\IPv6Address $ipv6_address
* @return boolean
*/
private static function ipV6IsPrivate(IPv6Address $ipv6_address): bool
{
return \collect(self::$private_ip_v6_cidr_list)
->concat([self::$private_ip_v6_loopback_cidr])
->map(fn (string $ipv6_cidr) => IPv6Range::Make($ipv6_cidr)->unwrap())
->reduce(fn (bool|null $initial, IPv6Range $ipv6_range) =>
$initial || $ipv6_range->containsAddress($ipv6_address));
}
/**
* Validate ip list
*
* @param array $ip_list
* @return void
* @throws \LDC\WebAdminSecureLaravel\Exceptions\AdminSecureInvalidAdressException If an IP is invalid.
*/
private static function validateIpList(array $ip_list): void
{
\collect($ip_list)->each(function ($ip) {
if (!\is_object($ip) or !\in_array(\get_class($ip), [IPv4Address::class, IPv6Address::class])) {
throw new AdminSecureInvalidAdressException('Invalid adresse ' . \strval(\json_encode($ip)));
}
});
}
/**
* Parse adresses
*
* @param \Illuminate\Http\Client\Response $response
* @return array<\Dxw\CIDR\IPAddress>
*/
private static function parseAdresses(Response $response): array
{
return \collect($response->json())
->map(fn (string $ip) => IPAddress::Make($ip)->unwrap())->all();
}
}