This commit is contained in:
2024-12-11 22:08:26 +02:00
parent bcbf807aac
commit 72674c6592
12 changed files with 1245 additions and 3 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/vendor
.env
.env
requests.sqlite3

View File

@@ -1,6 +1,15 @@
{
"require": {
"clue/reactphp-sqlite": "^1.6",
"clue/framework-x": "^0.16"
"clue/framework-x": "^0.16",
"vlucas/phpdotenv": "^5.6",
"react/cache": "^1.2",
"clue/mq-react": "^1.6",
"smarty/smarty": "^5.4"
},
"autoload": {
"psr-4": {
"XBotControl\\": "src/"
}
}
}

603
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "db1acaba19a0c8b768f5023662d59dc1",
"content-hash": "2ff629509c131622c46d8c67505d54b2",
"packages": [
{
"name": "clue/framework-x",
@@ -78,6 +78,77 @@
],
"time": "2024-03-05T14:41:18+00:00"
},
{
"name": "clue/mq-react",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/clue/reactphp-mq.git",
"reference": "cab0147723017bc2deb3f248c607ad8e3c87e509"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/reactphp-mq/zipball/cab0147723017bc2deb3f248c607ad8e3c87e509",
"reference": "cab0147723017bc2deb3f248c607ad8e3c87e509",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/promise": "^3 || ^2.2.1 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4 || ^3 || ^2",
"react/event-loop": "^1.2",
"react/http": "^1.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\React\\Mq\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"description": "Mini Queue, the lightweight in-memory message queue to concurrently do many (but not too many) things at once, built on top of ReactPHP",
"homepage": "https://github.com/clue/reactphp-mq",
"keywords": [
"Mini Queue",
"async",
"concurrency",
"job",
"message",
"message queue",
"queue",
"rate limit",
"reactphp",
"throttle",
"worker"
],
"support": {
"issues": "https://github.com/clue/reactphp-mq/issues",
"source": "https://github.com/clue/reactphp-mq/tree/v1.6.0"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2023-07-28T14:12:19+00:00"
},
{
"name": "clue/ndjson-react",
"version": "v1.3.0",
@@ -311,6 +382,68 @@
},
"time": "2020-11-24T22:02:12+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2024-07-20T21:45:45+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
@@ -361,6 +494,81 @@
},
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2024-07-20T21:41:07+00:00"
},
{
"name": "psr/http-message",
"version": "1.1",
@@ -1109,6 +1317,399 @@
}
],
"time": "2024-06-11T12:45:25+00:00"
},
{
"name": "smarty/smarty",
"version": "v5.4.2",
"source": {
"type": "git",
"url": "https://github.com/smarty-php/smarty.git",
"reference": "642a97adcc2bf6c1b2458d6afeeb36ae001c1c2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/smarty-php/smarty/zipball/642a97adcc2bf6c1b2458d6afeeb36ae001c1c2f",
"reference": "642a97adcc2bf6c1b2458d6afeeb36ae001c1c2f",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"symfony/polyfill-mbstring": "^1.27"
},
"require-dev": {
"phpunit/phpunit": "^8.5 || ^7.5",
"smarty/smarty-lexer": "^4.0.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Smarty\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0"
],
"authors": [
{
"name": "Monte Ohrt",
"email": "monte@ohrt.com"
},
{
"name": "Uwe Tews",
"email": "uwe.tews@googlemail.com"
},
{
"name": "Rodney Rehm",
"email": "rodney.rehm@medialize.de"
},
{
"name": "Simon Wisselink",
"homepage": "https://www.iwink.nl/"
}
],
"description": "Smarty - the compiling PHP template engine",
"homepage": "https://smarty-php.github.io/smarty/",
"keywords": [
"templating"
],
"support": {
"forum": "https://github.com/smarty-php/smarty/discussions",
"issues": "https://github.com/smarty-php/smarty/issues",
"source": "https://github.com/smarty-php/smarty/tree/v5.4.2"
},
"time": "2024-11-20T21:18:16+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2024-07-20T21:52:34+00:00"
}
],
"packages-dev": [],

BIN
requests.db Normal file

Binary file not shown.

31
src/Bot.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace XBotControl;
use Psr\Http\Message\ServerRequestInterface;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\PromiseInterface;
class Bot
{
// Helper Functions
public function isBot($userAgent): bool
{
$botKeywords = ['bot', 'crawl', 'spider', 'slurp', 'archive'];
foreach ($botKeywords as $keyword) {
if (stripos($userAgent, $keyword) !== false) {
return true;
}
}
return false;
}
public function extractBotName($userAgent): string|null
{
preg_match('/bot|crawl|spider|slurp|archive/i', $userAgent, $matches);
return $matches[0] ?? null;
}}

78
src/Config.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace XBotControl;
class Config
{
/**
* @var Config|null
*/
protected static $instance;
public $db;
public $smarty;
public function __construct()
{
$this->smarty = new \Smarty\Smarty();
$this->smarty->setTemplateDir(__DIR__ . '/../smarty/template/');
$this->smarty->setConfigDir(__DIR__ . '/../smarty/config/');
$this->smarty->setCompileDir(__DIR__ . '/../smarty/compile/');
$this->smarty->setCacheDir(__DIR__ . '/../smarty/cache/');
$this->smarty->setEscapeHtml(true);
$this->smarty->assign([
'baseURI' => $_ENV['BASEURI'],
]);
$this->smarty->compile_check = 1;
/* $this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/bots.db');
'uaCache' => new React\Cache\ArrayCache(1000),
'ipCache' => new React\Cache\ArrayCache(1000),
'headerCache' => new React\Cache\ArrayCache(1000),
'domainCache' => new React\Cache\ArrayCache(1000),
'pathCache' => new React\Cache\ArrayCache(1000), */
}
/**
* @return Config
*/
public static function getInstance()
{
if (empty(self::$instance)) self::$instance = new self();
return self::$instance;
}
public static function registerAssetRoutes(&$app)
{
// Define the directory to scan
$assetsDir = realpath(__DIR__ . '/../public/assets');
if ($assetsDir === false) {
throw new \Exception('Assets directory not found');
}
// Create a recursive directory iterator to scan all files
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($assetsDir)
);
// Iterate through all files in the assets directory
foreach ($iterator as $file) {
if ($file->isFile()) {
// Get the relative path of the file
$relativePath = str_replace($assetsDir, '', $file->getRealPath());
// Register the route
$route = $_ENV['BASEURI'] . '/assets' . str_replace('\\', '/', $relativePath);
$app->get($route, Controllers\StaticFilesController::class);
}
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace XBotControl\Controllers;
use Psr\Http\Message\ServerRequestInterface;
class StaticFilesController
{
/**
* Mapping between file extension and MIME type to send in `Content-Type` response header
*
* @var array<string,string>
*/
private $mimetypes = array(
'atom' => 'application/atom+xml',
'bz2' => 'application/x-bzip2',
'css' => 'text/css',
'gif' => 'image/gif',
'gz' => 'application/gzip',
'htm' => 'text/html',
'html' => 'text/html',
'ico' => 'image/x-icon',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'js' => 'text/javascript',
'json' => 'application/json',
'pdf' => 'application/pdf',
'png' => 'image/png',
'rss' => 'application/rss+xml',
'svg' => 'image/svg+xml',
'tar' => 'application/x-tar',
'xml' => 'application/xml',
'zip' => 'application/zip',
);
public function __invoke(ServerRequestInterface $request)
{
$path = $request->getUri()->getPath();
$cleanedPath = $_ENV['APP_DIR'] . '/public' . str_replace($_ENV['BASEURI'], '', $path);
$stream = new \React\Stream\ReadableResourceStream(fopen($cleanedPath, 'r'), null, 65536);
$ext = strtolower(substr($path, strrpos($path, '.') + 1));
return new \React\Http\Message\Response(
\React\Http\Message\Response::STATUS_OK,
[
'Content-Type' => $this->mimetypes[$ext] ?? 'text/html',
'Cache-Control' => 'max-age=15552000',
'Content-length' => filesize($cleanedPath),
'Expires' => gmdate('D, d M Y H:i:s T', strtotime('next month')),
'Date' => gmdate('D, d M Y H:i:s T', filemtime($cleanedPath)),
'Last-modified' => gmdate('D, d M Y H:i:s T',filectime($cleanedPath))
],
$stream
);
}
}

187
src/IPMatch.php Normal file
View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace XBotControl;
use Psr\Http\Message\ServerRequestInterface;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\PromiseInterface;
use React\Promise\Promise;
class IPMatch
{
/**
* Main function to check if a given IP (IPv4 or IPv6) matches a whitelist of ranges from the database.
*
* @param string $ip The IP address to check.
* @param object $db The database connection object (ReactPHP-based).
* @return \React\Promise\Promise Promise resolving to true if the IP matches any of the whitelist ranges, false otherwise.
*/
public static function checkIPWhitelistAsync(string $ip, $db): Promise
{
return self::getIPVersion($ip)->then(
function ($ipVersion) use ($ip, $db) {
if ($ipVersion === null) {
return false; // Invalid IP
}
return self::fetchWhitelistRanges($db, $ipVersion)->then(
function ($ranges) use ($ip, $ipVersion) {
if ($ipVersion === 'ipv4') {
return self::checkIPv4($ip, $ranges);
} elseif ($ipVersion === 'ipv6') {
return self::checkIPv6($ip, $ranges);
}
return false;
}
);
}
);
}
/**
* Determine the IP version (IPv4, IPv6, or null for invalid).
*
* @param string $ip The IP address to validate.
* @return \React\Promise\Promise Promise resolving to 'ipv4', 'ipv6', or null.
*/
private static function getIPVersion(string $ip): Promise
{
return new Promise(function (callable $resolve) use ($ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$resolve('ipv4');
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$resolve('ipv6');
} else {
$resolve(null);
}
});
}
/**
* Fetch whitelist ranges from the database for a specific IP version.
*
* @param object $db The database connection object.
* @param string $ipVersion The IP version ('ipv4' or 'ipv6').
* @return \React\Promise\Promise Promise resolving to an array of CIDR ranges.
*/
private static function fetchWhitelistRanges($db, string $ipVersion): Promise
{
return new Promise(function (callable $resolve, callable $reject) use ($db, $ipVersion) {
$query = 'SELECT range FROM whitelist WHERE ip_version = ?';
$db->query($query, [$ipVersion])->then(
function ($result) use ($resolve) {
$ranges = array_column($result->resultRows, 'range');
$resolve($ranges);
},
function ($error) use ($reject) {
$reject($error); // Reject if the query fails
}
);
});
}
/**
* Check if a given IPv6 address is in a network from an array of ranges (asynchronous with ReactPHP Promise)
*
* @param string $ip IPv6 address to check
* @param array $ranges Array of IPv6/CIDR ranges, e.g., ['2001:db8::/32', '2001:0db8:85a3::8a2e:0370:7334/128']
* @return \React\Promise\Promise Promise resolving to true if the IPv6 is in any of the ranges, false otherwise
*/
public static function checkIPv6(string $ip, array $ranges): PromiseInterface
{
return new Promise(function (callable $resolve) use ($ip, $ranges) {
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$resolve(false);
return;
}
foreach ($ranges as $range) {
if (!is_string($range)) {
continue;
}
if (strpos($range, '/') === false) {
$range .= '/128';
}
[$range_ip, $netmask] = explode('/', $range, 2);
if (!filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) || !is_numeric($netmask) || $netmask < 0 || $netmask > 128) {
continue; // Skip invalid ranges
}
$ip_bin = inet_pton($ip);
$range_bin = inet_pton($range_ip);
$netmask_bin = str_repeat("\xff", (int)($netmask / 8));
if ($netmask % 8 !== 0) {
$netmask_bin .= chr(0xff << (8 - ($netmask % 8)));
}
$netmask_bin = str_pad($netmask_bin, strlen($ip_bin), "\x00");
if (($ip_bin & $netmask_bin) === ($range_bin & $netmask_bin)) {
$resolve(true); // Resolve with true if a match is found
return;
}
}
$resolve(false); // Resolve with false if no matches are found
});
}
/**
* Check if a given IPv4 address is in a network from an array of ranges (asynchronous with ReactPHP Promise)
*
* @param string $ip IPv4 address to check
* @param array $ranges Array of IPv4/CIDR ranges, e.g., ['192.168.1.0/24', '10.0.0.1/32']
* @return \React\Promise\Promise Promise resolving to true if the IPv4 is in any of the ranges, false otherwise
*/
public static function checkIPv4(string $ip, array $ranges): Promise
{
return new Promise(function (callable $resolve) use ($ip, $ranges) {
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$resolve(false);
return;
}
foreach ($ranges as $range) {
if (!is_string($range)) {
continue;
}
if (strpos($range, '/') === false) {
$range .= '/32';
}
[$range_ip, $netmask] = explode('/', $range, 2);
if (!filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || !is_numeric($netmask) || $netmask < 0 || $netmask > 32) {
continue; // Skip invalid ranges
}
$range_decimal = ip2long($range_ip);
$ip_decimal = ip2long($ip);
$netmask_decimal = -1 << (32 - (int)$netmask);
if (($ip_decimal & $netmask_decimal) === ($range_decimal & $netmask_decimal)) {
$resolve(true); // Resolve with true if a match is found
return;
}
}
$resolve(false); // Resolve with false if no matches are found
});
}
}

53
src/InitTables.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace XBotControl;
use React\Promise\PromiseInterface;
class InitTables
{
public static function create():PromiseInterface
{
$db = Storage::getInstance()->db;
return $db->exec("CREATE TABLE IF NOT EXISTS ip (data TEXT UNIQUE NOT NULL CHECK (data LIKE '%'), CONSTRAINT valid_ip CHECK (data LIKE '%.%' OR data LIKE '%:%')) STRICT ;")
->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS domain (data TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS path (data TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS useragent ( data TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS headers ( data TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec("CREATE TABLE IF NOT EXISTS networkwhitelist ( data TEXT UNIQUE NOT NULL CHECK (data LIKE '%/%'), CONSTRAINT valid_network CHECK ( data LIKE '%.%/%' OR data LIKE '%:%/%' )) STRICT ;");
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS request ( id_ip INTEGER NOT NULL, id_method INTEGER NOT NULL, id_domain INTEGER NOT NULL, id_path INTEGER NOT NULL, id_useragent INTEGER NOT NULL, id_headers INTEGER NOT NULL, timestamp INTEGER NOT NULL, FOREIGN KEY (id_ip) REFERENCES ip(rowid), FOREIGN KEY (id_domain) REFERENCES domain(rowid), FOREIGN KEY (id_path) REFERENCES path(rowid), FOREIGN KEY (id_useragent) REFERENCES useragent(rowid), FOREIGN KEY (id_headers) REFERENCES headers(rowid) ) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS bot ( name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
})->then(function () use ($db) {
return $db->exec('PRAGMA journal_mode=WAL;');
});
return $db->exec("CREATE TABLE IF NOT EXISTS ip (id_ip INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT UNIQUE NOT NULL CHECK (ip LIKE '%'), CONSTRAINT valid_ip CHECK (ip LIKE '%.%' OR ip LIKE '%:%')) STRICT ;")
->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS domain (id_domain INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS path ( id_path INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS useragent ( id_useragent INTEGER PRIMARY KEY AUTOINCREMENT, useragent TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS headers ( id_headers INTEGER PRIMARY KEY AUTOINCREMENT, headers TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec("CREATE TABLE IF NOT EXISTS networkwhitelist ( id_networkwhitelist INTEGER PRIMARY KEY AUTOINCREMENT, network TEXT UNIQUE NOT NULL CHECK (network LIKE '%/%'), CONSTRAINT valid_network CHECK ( network LIKE '%.%/%' OR network LIKE '%:%/%' )) STRICT WITHOUT ROWID ;");
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS request ( id_request INTEGER PRIMARY KEY AUTOINCREMENT, id_ip INTEGER NOT NULL, id_method INTEGER NOT NULL, id_domain INTEGER NOT NULL, id_path INTEGER NOT NULL, id_useragent INTEGER NOT NULL, id_headers INTEGER NOT NULL, timestamp INTEGER NOT NULL, FOREIGN KEY (id_ip) REFERENCES ip(id_ip), FOREIGN KEY (id_domain) REFERENCES domain(id_domain), FOREIGN KEY (id_path) REFERENCES path(id_path), FOREIGN KEY (id_useragent) REFERENCES useragent(id_useragent), FOREIGN KEY (id_headers) REFERENCES headers(id_headers) ) STRICT WITHOUT ROWID ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS bot ( id_bot INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
})->then(function () use ($db) {
return $db->exec('PRAGMA journal_mode=WAL;');
});
}
}

78
src/Request.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace XBotControl;
use Psr\Http\Message\ServerRequestInterface;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\PromiseInterface;
use React\Promise\Promise;
class Request
{
const METHOD = [
'GET' => 1,
'HEAD' => 2,
'OPTIONS' => 3,
'TRACE' => 4,
'PUT' => 5,
'DELETE' => 6,
'POST' => 7,
'PATCH' => 8,
'CONNECT' => 9
];
public static function save(ServerRequestInterface $request): PromiseInterface
{
$realIp = self::getRealIP($request);
$userAgent = $request->getHeaderLine('User-Agent') ?: 'Unknown';
$headers = json_encode($request->getHeaders(), JSON_UNESCAPED_UNICODE);
$uri = $request->getUri();
$storage = Storage::getInstance();
// Use parallel promises for ID generation to avoid waiting for each in sequence
$idPromises = [
'id_ip' => $storage::getId('ip', $realIp),
'id_domain' => $storage::getId('domain', $uri->getHost()),
'id_path' => $storage::getId('path', $uri->getPath()),
'id_useragent' => $storage::getId('useragent', $userAgent),
'id_headers' => $storage::getId('headers', md5($headers)),
];
return \React\Promise\all($idPromises)
->then(function ($resolvedValues) use ($request, $storage) {
// Set resolved values efficiently
$resolvedValues['id_method'] = self::METHOD[$request->getMethod()] ?? 0;
$resolvedValues['timestamp'] = time();
// Directly save data asynchronously
return $storage::insert('request', $resolvedValues);
})
->then(function () {
return \React\Http\Message\Response::plaintext('');
});
}
public static function getRealIP(ServerRequestInterface $request): string
{
$cfConnectingIp = $request->getHeaderLine('CF-Connecting-IP');
if (!empty($cfConnectingIp)) {
return $cfConnectingIp;
}
$xForwardedFor = $request->getHeaderLine('X-Forwarded-For');
if (!empty($xForwardedFor)) {
return explode(',', $xForwardedFor)[0];
}
$remoteAddr = $request->getServerParams()['REMOTE_ADDR'] ?? '0.0.0.0';
return $remoteAddr;
}
}

100
src/Storage.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace XBotControl;
use Clue\React\SQLite\DatabaseInterface;
use React\Promise\PromiseInterface;
use Clue\React\SQLite\Result;
class Storage
{
protected static $instance;
/** @var DatabaseInterface $db */
public $db;
public $cache = [];
protected static $tablesCache = [
'ip',
'domain',
'useragent',
'headers',
'path'
];
public function __construct()
{
$this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/requests.db');
foreach (self::$tablesCache as $cacheParition) {
$this->cache[$cacheParition] = new \React\Cache\ArrayCache(1000);
}
}
public static function getInstance(): Storage
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public static function getId(string $cacheParition, string $key): PromiseInterface
{
$storage = self::getInstance();
/* if (!isset(self::$tablesCache[$cacheParition])) {
return $storage::insert($cacheParition, ['data' => $key])->then(function ($result) {
return $result->rows["0"][$result->columns['0']];
});
} */
return $storage->cache[$cacheParition]->get($key)
->then(function ($result) use ($storage, $cacheParition, $key) {
if ($result === null) {
return self::insertAndCache($storage, $cacheParition, $key);
}
return (int) $result;
}, function () {
return 0;
});
}
private static function insertAndCache(Storage $storage, string $cacheParition, string $key): PromiseInterface
{
$query = "INSERT INTO $cacheParition (data) VALUES (?) ON CONFLICT(data) DO UPDATE SET data=? RETURNING rowid ;";
return $storage->db
->query($query, [$key, $key])
->then(function (Result $result) use ($storage, $cacheParition, $key) {
return self::cache($storage, $cacheParition, $key, $result);
});
}
private static function cache(Storage $storage, string $cacheParition, string $key, Result $result): PromiseInterface
{
$value = $result->rows["0"][$result->columns['0']];
return $storage->cache[$cacheParition]->set($key, $value)
->then(function () use ($value) {
return $value;
});
}
public static function insert(string $table, array $values): PromiseInterface
{
$columns = implode(", ", array_keys($values));
$placeholders = implode(", ", array_fill(0, count($values), "?"));
$query = sprintf("INSERT INTO %s (%s) VALUES (%s);", $table, $columns, $placeholders);
$params = array_values($values);
$storage = self::getInstance();
return $storage->db->query($query, $params)->then(function (Result $result) {
return $result->insertId;
});
}
}

41
xbotcontrol.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$_ENV['APP_DIR'] = __DIR__;
$_ENV['X_LISTEN'] = '0.0.0.0:7500';
XBotControl\InitTables::create();
$app = new FrameworkX\App();
$app->get(
'/mirror',
function (Psr\Http\Message\ServerRequestInterface $request) {
return XBotControl\Request::save($request);
}
);
$app->get('/mirror1', function () {
return React\Http\Message\Response::plaintext(
"Hello wörld!\n"
);
});
$app->get('/cp', function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext(
"Hello " . $request->getAttribute('name') . "!\n"
);
});
$app->run();
XBotControl\Storage::getInstance()->db->query('PRAGMA main.wal_checkpoint;')
->then(function () {
XBotControl\Storage::getInstance()->db->quit();
});