2023-06-02 16:51:52 +02:00
|
|
|
<?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';
|
|
|
|
|
|
2023-06-02 17:49:33 +02:00
|
|
|
/**
|
|
|
|
|
* The cache key where to store config cidr list.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private static string $cache_config_key = 'ldcweb_ip_cidr_config_list';
|
|
|
|
|
|
2023-06-02 16:51:52 +02:00
|
|
|
/**
|
|
|
|
|
* 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()) {
|
2023-06-02 17:49:33 +02:00
|
|
|
throw new AdminSecureException(\trans('Client IP not valid ') . $ip->getErr());
|
2023-06-02 16:51:52 +02:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-02 17:49:33 +02:00
|
|
|
/**
|
|
|
|
|
* 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');
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-02 16:51:52 +02:00
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
}
|