Initial commit

This commit is contained in:
Rekryt
2024-08-30 15:22:24 +03:00
commit 171e449744
46 changed files with 4638 additions and 0 deletions

115
src/Domain/Entity/Site.php Normal file
View 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);
}
}

View 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))
);
}
}

View 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];
}
}

View 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;
}
}

View 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;
}
}