142 lines
3.3 KiB
PHP
142 lines
3.3 KiB
PHP
<?php
|
||
|
||
/**
|
||
* Czy REMOTE_ADDR to lokalny hop / reverse proxy, któremu wolno
|
||
* przekazać prawdziwy IP klienta w X-Forwarded-For.
|
||
*/
|
||
function isTrustedForwardingHop(string $remoteAddr): bool
|
||
{
|
||
$ip = normalizeClientIp($remoteAddr);
|
||
if ($ip === '') {
|
||
return false;
|
||
}
|
||
|
||
if (in_array($ip, ['127.0.0.1', '::1'], true)) {
|
||
return true;
|
||
}
|
||
|
||
// Proxy / Apache na LAN restauracji (goście Wi‑Fi widziani jako 10.x w XFF)
|
||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||
foreach (['10.0.0.0/8', '192.168.0.0/16', '172.16.0.0/12'] as $cidr) {
|
||
if (ipv4InCidr($ip, $cidr)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Adres IP klienta.
|
||
* X-Forwarded-For uznajemy wyłącznie, gdy połączenie przychodzi z zaufanego
|
||
* hopa (localhost / LAN) — inaczej każdy w internecie mógłby podrobić IP
|
||
* i ominąć geo. Bezpośredni dostęp z internetu = samo REMOTE_ADDR.
|
||
*/
|
||
function getRequestClientIp(): string
|
||
{
|
||
$remote = trim((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||
|
||
if (isTrustedForwardingHop($remote) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||
$parts = explode(',', (string) $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||
$forwarded = normalizeClientIp(trim($parts[0]));
|
||
if ($forwarded !== '' && filter_var($forwarded, FILTER_VALIDATE_IP)) {
|
||
return $forwarded;
|
||
}
|
||
}
|
||
|
||
return normalizeClientIp($remote);
|
||
}
|
||
|
||
/**
|
||
* Hostnamey, dla których frontend pomija geolokalizację (dev / znany host).
|
||
* Wstrzykiwane do window.APP_CONFIG przez app.php.
|
||
*/
|
||
function getGeoBypassTrustedHosts(): array
|
||
{
|
||
return [
|
||
'82.160.190.247',
|
||
'localhost',
|
||
'127.0.0.1',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Pojedyncze IP z pominięciem geo (zewnętrzne, dev, przykładowe hosty LAN).
|
||
*/
|
||
function getGeoBypassTrustedIps(): array
|
||
{
|
||
return [
|
||
'82.160.190.247',
|
||
'127.0.0.1',
|
||
'::1',
|
||
'192.168.20.84',
|
||
'10.0.0.3',
|
||
'10.0.0.7',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Pule wewnętrznych sieci — goście na Wi‑Fi restauracji.
|
||
*/
|
||
function getGeoBypassTrustedCidrs(): array
|
||
{
|
||
return [
|
||
'10.0.0.0/24',
|
||
];
|
||
}
|
||
|
||
function normalizeClientIp(string $ip): string
|
||
{
|
||
if (strpos($ip, '::ffff:') === 0) {
|
||
return substr($ip, 7);
|
||
}
|
||
|
||
return $ip;
|
||
}
|
||
|
||
function ipv4InCidr(string $ip, string $cidr): bool
|
||
{
|
||
if (!str_contains($cidr, '/')) {
|
||
return false;
|
||
}
|
||
|
||
[$subnet, $bits] = explode('/', $cidr, 2);
|
||
$bits = (int) $bits;
|
||
if ($bits < 0 || $bits > 32) {
|
||
return false;
|
||
}
|
||
|
||
$ipLong = ip2long($ip);
|
||
$subnetLong = ip2long($subnet);
|
||
if ($ipLong === false || $subnetLong === false) {
|
||
return false;
|
||
}
|
||
|
||
$mask = $bits === 0 ? 0 : (-1 << (32 - $bits)) & 0xFFFFFFFF;
|
||
|
||
return ($ipLong & $mask) === ($subnetLong & $mask);
|
||
}
|
||
|
||
function isGeoBypassTrustedIp(string $ip): bool
|
||
{
|
||
$ip = normalizeClientIp($ip);
|
||
if ($ip === '') {
|
||
return false;
|
||
}
|
||
|
||
if (in_array($ip, getGeoBypassTrustedIps(), true)) {
|
||
return true;
|
||
}
|
||
|
||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||
foreach (getGeoBypassTrustedCidrs() as $cidr) {
|
||
if (ipv4InCidr($ip, $cidr)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|