diff --git a/.gitignore b/.gitignore index 28d9e18..4c8ad88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor -.env \ No newline at end of file +.env +requests.sqlite3 \ No newline at end of file diff --git a/composer.json b/composer.json index 8d0cb42..fb5951b 100644 --- a/composer.json +++ b/composer.json @@ -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/" + } } } diff --git a/composer.lock b/composer.lock index 3d65051..a0ad34e 100644 --- a/composer.lock +++ b/composer.lock @@ -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": [], diff --git a/requests.db b/requests.db new file mode 100644 index 0000000..ea7cd0a Binary files /dev/null and b/requests.db differ diff --git a/src/Bot.php b/src/Bot.php new file mode 100644 index 0000000..5725503 --- /dev/null +++ b/src/Bot.php @@ -0,0 +1,31 @@ +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); + } + } + } + + +} diff --git a/src/Controllers/StaticFilesController.php b/src/Controllers/StaticFilesController.php new file mode 100644 index 0000000..2b91ed2 --- /dev/null +++ b/src/Controllers/StaticFilesController.php @@ -0,0 +1,63 @@ + + */ + 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 + ); + } +} diff --git a/src/IPMatch.php b/src/IPMatch.php new file mode 100644 index 0000000..489f393 --- /dev/null +++ b/src/IPMatch.php @@ -0,0 +1,187 @@ +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 + }); + } +} diff --git a/src/InitTables.php b/src/InitTables.php new file mode 100644 index 0000000..d11165a --- /dev/null +++ b/src/InitTables.php @@ -0,0 +1,53 @@ +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;'); + }); + } +} diff --git a/src/Request.php b/src/Request.php new file mode 100644 index 0000000..bc13ae0 --- /dev/null +++ b/src/Request.php @@ -0,0 +1,78 @@ + 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; + } +} diff --git a/src/Storage.php b/src/Storage.php new file mode 100644 index 0000000..dac45e0 --- /dev/null +++ b/src/Storage.php @@ -0,0 +1,100 @@ +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; + }); + } +} diff --git a/xbotcontrol.php b/xbotcontrol.php new file mode 100644 index 0000000..3b3d75d --- /dev/null +++ b/xbotcontrol.php @@ -0,0 +1,41 @@ +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(); + });