mirror of
https://github.com/rekryt/iplist.git
synced 2025-10-13 08:59:34 +03:00
Initial commit
This commit is contained in:
115
src/Domain/Entity/Site.php
Normal file
115
src/Domain/Entity/Site.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Entity;
|
||||
|
||||
use OpenCCK\Domain\Factory\SiteFactory;
|
||||
use OpenCCK\Domain\Helper\DNSHelper;
|
||||
use OpenCCK\Domain\Helper\IP4Helper;
|
||||
use OpenCCK\Domain\Helper\IP6Helper;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Revolt\EventLoop;
|
||||
use stdClass;
|
||||
|
||||
final class Site {
|
||||
private DNSHelper $dnsHelper;
|
||||
|
||||
/**
|
||||
* @param string $name Name of portal
|
||||
* @param array $domains List of portal domains
|
||||
* @param array $dns List of DNS servers for updating IP addresses
|
||||
* @param int $timeout Time interval between domain IP address updates (seconds)
|
||||
* @param array $ip4 List of IPv4 addresses
|
||||
* @param array $ip6 List of IPv6 addresses
|
||||
* @param array $cidr4 List of CIDRv4 zones of IPv4 addresses
|
||||
* @param array $cidr6 List of CIDRv6 zones of IPv6 addresses
|
||||
* @param object $external Lists of URLs to retrieve data from external sources
|
||||
*
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public array $domains = [],
|
||||
public array $dns = [],
|
||||
public int $timeout = 1440 * 60,
|
||||
public array $ip4 = [],
|
||||
public array $ip6 = [],
|
||||
public array $cidr4 = [],
|
||||
public array $cidr6 = [],
|
||||
public object $external = new stdClass()
|
||||
) {
|
||||
$this->dnsHelper = new DNSHelper($dns);
|
||||
EventLoop::delay(0, $this->reload(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function reload(): void {
|
||||
$ip4 = [];
|
||||
$ip6 = [];
|
||||
foreach ($this->domains as $domain) {
|
||||
[$ipv4results, $ipv6results] = $this->dnsHelper->resolve($domain);
|
||||
$ip4 = array_merge($ip4, $ipv4results);
|
||||
$ip6 = array_merge($ip6, $ipv6results);
|
||||
}
|
||||
|
||||
$newIp4 = SiteFactory::normalize(array_diff($ip4, $this->ip4));
|
||||
$this->cidr4 = SiteFactory::normalize(IP4Helper::processCIDR($newIp4, $this->cidr4));
|
||||
|
||||
$newIp6 = SiteFactory::normalize(array_diff($ip6, $this->ip6));
|
||||
$this->cidr6 = SiteFactory::normalize(IP6Helper::processCIDR($newIp6, $this->cidr6));
|
||||
|
||||
$this->ip4 = SiteFactory::normalize(array_merge($this->ip4, $ip4));
|
||||
$this->ip6 = SiteFactory::normalize(array_merge($this->ip6, $ip6));
|
||||
|
||||
App::getLogger()->debug('Reloaded for ' . $this->name);
|
||||
|
||||
EventLoop::delay($this->timeout, function () {
|
||||
$this->reloadExternal();
|
||||
$this->reload();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function reloadExternal(): void {
|
||||
if (isset($this->external->domains)) {
|
||||
foreach ($this->external->domains as $url) {
|
||||
$this->domains = SiteFactory::normalize(
|
||||
array_merge($this->domains, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->ip4)) {
|
||||
foreach ($this->external->ip4 as $url) {
|
||||
$this->ip4 = SiteFactory::normalize(array_merge($this->ip4, explode("\n", file_get_contents($url))));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->ip6)) {
|
||||
foreach ($this->external->ip6 as $url) {
|
||||
$this->ip6 = SiteFactory::normalize(array_merge($this->ip6, explode("\n", file_get_contents($url))));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->cidr4)) {
|
||||
foreach ($this->external->cidr4 as $url) {
|
||||
$this->cidr4 = SiteFactory::normalize(
|
||||
array_merge($this->cidr4, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->cidr6)) {
|
||||
foreach ($this->external->cidr6 as $url) {
|
||||
$this->cidr6 = SiteFactory::normalize(
|
||||
array_merge($this->cidr6, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App::getLogger()->debug('External reloaded for ' . $this->name);
|
||||
}
|
||||
}
|
77
src/Domain/Factory/SiteFactory.php
Normal file
77
src/Domain/Factory/SiteFactory.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Factory;
|
||||
|
||||
use OpenCCK\Domain\Entity\Site;
|
||||
use OpenCCK\Domain\Helper\IP4Helper;
|
||||
use OpenCCK\Domain\Helper\IP6Helper;
|
||||
use stdClass;
|
||||
|
||||
class SiteFactory {
|
||||
/**
|
||||
* @param string $name Name of portal
|
||||
* @param object $config Configuration of portal
|
||||
* @return Site
|
||||
*
|
||||
*/
|
||||
static function create(string $name, object $config): Site {
|
||||
$domains = $config->domains ?? [];
|
||||
$dns = $config->dns ?? [];
|
||||
$timeout = $config->timeout ?? 1440 * 60;
|
||||
$ip4 = $config->ip4 ?? [];
|
||||
$ip6 = $config->ip6 ?? [];
|
||||
$cidr4 = $config->cidr4 ?? [];
|
||||
$cidr6 = $config->cidr6 ?? [];
|
||||
$external = $config->external ?? new stdClass();
|
||||
|
||||
if (isset($external)) {
|
||||
if (isset($external->domains)) {
|
||||
foreach ($external->domains as $url) {
|
||||
$domains = array_merge($domains, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->ip4)) {
|
||||
foreach ($external->ip4 as $url) {
|
||||
$ip4 = array_merge($ip4, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->ip6)) {
|
||||
foreach ($external->ip6 as $url) {
|
||||
$ip6 = array_merge($ip6, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->cidr4)) {
|
||||
foreach ($external->cidr4 as $url) {
|
||||
$cidr4 = array_merge($cidr4, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->cidr6)) {
|
||||
foreach ($external->cidr6 as $url) {
|
||||
$cidr6 = array_merge($cidr6, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$domains = self::normalize($domains);
|
||||
$ip4 = self::normalize($ip4);
|
||||
$ip6 = self::normalize($ip6);
|
||||
$cidr4 = self::normalize(IP4Helper::processCIDR($ip4, self::normalize($cidr4)));
|
||||
$cidr6 = self::normalize(IP6Helper::processCIDR($ip6, self::normalize($cidr6)));
|
||||
|
||||
return new Site($name, $domains, $dns, $timeout, $ip4, $ip6, $cidr4, $cidr6, $external);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public static function normalize(array $array): array {
|
||||
return array_values(
|
||||
array_unique(array_filter($array, fn(string $item) => !str_starts_with($item, '#') && strlen($item) > 0))
|
||||
);
|
||||
}
|
||||
}
|
69
src/Domain/Helper/DNSHelper.php
Normal file
69
src/Domain/Helper/DNSHelper.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use Amp\Dns\DnsConfig;
|
||||
use Amp\Dns\DnsConfigLoader;
|
||||
use Amp\Dns\DnsException;
|
||||
use Amp\Dns\DnsRecord;
|
||||
use Amp\Dns\HostLoader;
|
||||
use Amp\Dns\Rfc1035StubDnsResolver;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use function Amp\Dns\dnsResolver;
|
||||
use function Amp\Dns\resolve;
|
||||
|
||||
readonly class DNSHelper {
|
||||
public function __construct(private array $dnsServers = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $dnsServers
|
||||
* @return void
|
||||
*/
|
||||
private function setResolver(array $dnsServers): void {
|
||||
dnsResolver(
|
||||
new Rfc1035StubDnsResolver(
|
||||
null,
|
||||
new class ($dnsServers) implements DnsConfigLoader {
|
||||
public function __construct(private readonly array $dnsServers = []) {
|
||||
}
|
||||
|
||||
public function loadConfig(): DnsConfig {
|
||||
return new DnsConfig($this->dnsServers, (new HostLoader())->loadHosts());
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @return array[]
|
||||
*/
|
||||
public function resolve(string $domain): array {
|
||||
$ipv4 = [];
|
||||
$ipv6 = [];
|
||||
foreach ($this->dnsServers as $server) {
|
||||
$this->setResolver([$server]);
|
||||
try {
|
||||
$ipv4 = array_merge(
|
||||
$ipv4,
|
||||
array_map(fn(DnsRecord $record) => $record->getValue(), resolve($domain, DnsRecord::A))
|
||||
);
|
||||
} catch (DnsException $e) {
|
||||
App::getLogger()->error($e->getMessage(), [$server]);
|
||||
}
|
||||
try {
|
||||
$ipv6 = array_merge(
|
||||
$ipv6,
|
||||
array_map(fn(DnsRecord $record) => $record->getValue(), resolve($domain, DnsRecord::AAAA))
|
||||
);
|
||||
} catch (DnsException $e) {
|
||||
App::getLogger()->error($e->getMessage(), [$server]);
|
||||
}
|
||||
}
|
||||
App::getLogger()->debug('resolve: ' . $domain, [count($ipv4), count($ipv6)]);
|
||||
return [$ipv4, $ipv6];
|
||||
}
|
||||
}
|
144
src/Domain/Helper/IP4Helper.php
Normal file
144
src/Domain/Helper/IP4Helper.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\Storage\CIDRStorage;
|
||||
|
||||
use function Amp\async;
|
||||
use function Amp\delay;
|
||||
|
||||
class IP4Helper {
|
||||
public static function processCIDR(array $ips, $results = []): array {
|
||||
$count = count($ips);
|
||||
foreach ($ips as $i => $ip) {
|
||||
if ($ip === '127.0.0.1') {
|
||||
continue;
|
||||
}
|
||||
async(function () use ($ip, $i, $count, &$results) {
|
||||
if (CIDRStorage::getInstance()->has($ip)) {
|
||||
$searchArray = CIDRStorage::getInstance()->get($ip);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isInRange($ip, $results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = null;
|
||||
$result = shell_exec('whois ' . $ip . ' | grep CIDR');
|
||||
if ($result) {
|
||||
preg_match('/^CIDR:\s*(.*)$/m', $result, $matches);
|
||||
$search = $matches[1] ?? null;
|
||||
}
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep inetnum',
|
||||
'head -n 1',
|
||||
"awk '{print $2\"-\"$4}'",
|
||||
'sed "s/-$//"',
|
||||
'xargs ipcalc',
|
||||
"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep IPv4',
|
||||
'grep " - "',
|
||||
'head -n 1',
|
||||
"awk '{print $3\"-\"$5}'",
|
||||
'xargs ipcalc',
|
||||
"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search = strtr($search, ["\n" => ' ', ', ' => ' ']);
|
||||
$searchArray = array_filter(
|
||||
explode(' ', strtr($search, ' ', '')),
|
||||
fn(string $cidr) => strlen($cidr) > 0
|
||||
);
|
||||
CIDRStorage::getInstance()->set($ip, $searchArray);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
} else {
|
||||
App::getLogger()->error($ip . ' -> CIDR not found', [$i + 1 . '/' . $count]);
|
||||
}
|
||||
delay(0.001);
|
||||
})->await();
|
||||
}
|
||||
|
||||
return self::minimizeSubnets($results);
|
||||
}
|
||||
|
||||
public static function trimCIDRs(array $searchArray): array {
|
||||
$subnets = [];
|
||||
foreach ($searchArray as $search) {
|
||||
foreach (explode(' ', $search) as $cidr) {
|
||||
if (str_contains($cidr, '/')) {
|
||||
$subnets[] = trim($cidr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
public static function sortSubnets(array $subnets): array {
|
||||
usort($subnets, function ($a, $b) {
|
||||
return (int) explode('/', $a)[1] - (int) explode('/', $b)[1];
|
||||
});
|
||||
usort($subnets, function ($a, $b) {
|
||||
return ip2long(explode('/', $a)[0]) - ip2long(explode('/', $b)[0]);
|
||||
});
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
public static function minimizeSubnets(array $subnets): array {
|
||||
$result = [];
|
||||
foreach (self::sortSubnets(array_filter($subnets, fn(string $subnet) => !!$subnet)) as $subnet) {
|
||||
$include = true;
|
||||
[$ip /*, $mask*/] = explode('/', $subnet);
|
||||
$ipLong = ip2long($ip);
|
||||
// $maskLong = ~((1 << 32 - (int) $mask) - 1);
|
||||
|
||||
foreach ($result as $resSubnet) {
|
||||
[$resIp, $resMask] = explode('/', $resSubnet);
|
||||
$resIpLong = ip2long($resIp);
|
||||
$resMaskLong = ~((1 << 32 - (int) $resMask) - 1);
|
||||
|
||||
if (($ipLong & $resMaskLong) === ($resIpLong & $resMaskLong)) {
|
||||
$include = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($include) {
|
||||
$result[] = $subnet;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function isInRange(string $ip, array $cidrs): bool {
|
||||
foreach ($cidrs as $cidr) {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
if ((ip2long($ip) & ~((1 << 32 - $mask) - 1)) === ip2long($subnet)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
176
src/Domain/Helper/IP6Helper.php
Normal file
176
src/Domain/Helper/IP6Helper.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\Storage\CIDRStorage;
|
||||
|
||||
use function Amp\async;
|
||||
use function Amp\delay;
|
||||
|
||||
class IP6Helper {
|
||||
public static function processCIDR(array $ips, $results = []): array {
|
||||
$count = count($ips);
|
||||
foreach ($ips as $i => $ip) {
|
||||
if ($ip === '::1') {
|
||||
continue;
|
||||
}
|
||||
async(function () use ($ip, $i, $count, &$results) {
|
||||
if (CIDRStorage::getInstance()->has($ip)) {
|
||||
$searchArray = CIDRStorage::getInstance()->get($ip);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isInRange($ip, $results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep inet6num',
|
||||
'grep -v "/0"',
|
||||
'head -n 1',
|
||||
"awk '{print $2}'",
|
||||
])
|
||||
);
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep route6',
|
||||
'grep -v "/0"',
|
||||
'head -n 1',
|
||||
"awk '{print $2}'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search = strtr($search, ["\n" => ' ', ', ' => ' ']);
|
||||
$searchArray = array_filter(
|
||||
explode(' ', strtr($search, ' ', '')),
|
||||
fn(string $cidr) => strlen($cidr) > 0
|
||||
);
|
||||
CIDRStorage::getInstance()->set($ip, $searchArray);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
} else {
|
||||
App::getLogger()->error($ip . ' -> CIDR not found', [$i + 1 . '/' . $count]);
|
||||
}
|
||||
delay(0.001);
|
||||
})->await();
|
||||
}
|
||||
|
||||
//return self::minimizeSubnets($results);
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $searchArray
|
||||
* @return array
|
||||
*/
|
||||
public static function trimCIDRs(array $searchArray): array {
|
||||
$subnets = [];
|
||||
foreach ($searchArray as $search) {
|
||||
foreach (explode(' ', $search) as $cidr) {
|
||||
if (str_contains($cidr, '/')) {
|
||||
[$address, $prefix] = explode('/', trim($cidr));
|
||||
$subnets[] = $address . '/' . max($prefix, 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $subnets
|
||||
* @return array
|
||||
*/
|
||||
public static function sortSubnets(array $subnets): array {
|
||||
usort($subnets, function ($a, $b) {
|
||||
return (int) explode('/', $a)[1] - (int) explode('/', $b)[1];
|
||||
});
|
||||
|
||||
usort($subnets, function ($a, $b) {
|
||||
$ipA = inet_pton(explode('/', $a)[0]);
|
||||
$ipB = inet_pton(explode('/', $b)[0]);
|
||||
|
||||
return strcmp($ipA, $ipB);
|
||||
});
|
||||
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $subnets
|
||||
* @return array
|
||||
*/
|
||||
public static function minimizeSubnets(array $subnets): array {
|
||||
$result = [];
|
||||
foreach (self::sortSubnets(array_filter($subnets, fn(string $subnet) => !!$subnet)) as $subnet) {
|
||||
if (!$subnet) {
|
||||
continue;
|
||||
}
|
||||
[$address, $prefix] = explode('/', $subnet);
|
||||
|
||||
$addressNum = inet_pton($address);
|
||||
$addressNum = unpack('J', $addressNum)[1];
|
||||
|
||||
$isUnique = true;
|
||||
foreach ($result as $existingCidr) {
|
||||
[$existingAddress, $existingPrefix] = explode('/', $existingCidr);
|
||||
$existingAddressNum = inet_pton($existingAddress);
|
||||
$existingAddressNum = unpack('J', $existingAddressNum)[1];
|
||||
|
||||
$mask = (1 << 128) - (1 << 128 - $prefix);
|
||||
$existingMask = (1 << 128) - (1 << 128 - $existingPrefix);
|
||||
if (($addressNum & $mask) === ($existingAddressNum & $existingMask)) {
|
||||
$isUnique = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isUnique) {
|
||||
$result[] = $subnet;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function isInRange(string $ip, array $cidrs): bool {
|
||||
$ip = inet_pton($ip);
|
||||
|
||||
foreach ($cidrs as $cidr) {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
$subnet = inet_pton($subnet);
|
||||
|
||||
$mask = intval($mask);
|
||||
$binaryMask = str_repeat('f', $mask >> 2);
|
||||
switch ($mask % 4) {
|
||||
case 1:
|
||||
$binaryMask .= '8';
|
||||
break;
|
||||
case 2:
|
||||
$binaryMask .= 'c';
|
||||
break;
|
||||
case 3:
|
||||
$binaryMask .= 'e';
|
||||
break;
|
||||
}
|
||||
$binaryMask = str_pad($binaryMask, 32, '0');
|
||||
$mask = pack('H*', $binaryMask);
|
||||
|
||||
if (($ip & $mask) === ($subnet & $mask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user