modified: xbotcontrol.php
This commit is contained in:
25
src/Classes/LoadStat.php
Normal file
25
src/Classes/LoadStat.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
|
||||
class LoadStat
|
||||
{
|
||||
|
||||
public static function saveLoad1()
|
||||
{
|
||||
$load = sys_getloadavg();
|
||||
if (!$load) {
|
||||
return;
|
||||
}
|
||||
$query = "INSERT OR IGNORE INTO load (load1, rowid) VALUES (?,?);";
|
||||
$params = [$load['0'], time()];
|
||||
|
||||
\XBotControl\Storage::getInstance()->db->query($query, $params);
|
||||
}
|
||||
}
|
||||
132
src/Classes/Report.php
Normal file
132
src/Classes/Report.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class Report
|
||||
{
|
||||
|
||||
public static function latest_requests(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = [
|
||||
[
|
||||
'title' => 'id',
|
||||
'field' => 'id',
|
||||
'visible' => false,
|
||||
'sortable' => true,
|
||||
'filterControl' => 'input',
|
||||
'widthUnit' => 'input',
|
||||
'width' => 'input',
|
||||
],
|
||||
[
|
||||
'sortable' => true,
|
||||
'title' => 'ip',
|
||||
'field' => 'ip',
|
||||
'sortable' => true,
|
||||
'filterControl' => 'input',
|
||||
|
||||
],
|
||||
[
|
||||
'sortable' => true,
|
||||
'title' => 'domain',
|
||||
'field' => 'domain',
|
||||
'sortable' => true,
|
||||
'visible' => false,
|
||||
'filterControl' => 'input',
|
||||
|
||||
],
|
||||
[
|
||||
'sortable' => true,
|
||||
'title' => 'path',
|
||||
'field' => 'path',
|
||||
'sortable' => true,
|
||||
'filterControl' => 'input',
|
||||
|
||||
],
|
||||
[
|
||||
'sortable' => true,
|
||||
'title' => 'useragent',
|
||||
'field' => 'useragent',
|
||||
'sortable' => true,
|
||||
'filterControl' => 'input',
|
||||
|
||||
],
|
||||
[
|
||||
'sortable' => true,
|
||||
'title' => 'load',
|
||||
'field' => 'load',
|
||||
'sortable' => true,
|
||||
'filterControl' => 'input',
|
||||
|
||||
],
|
||||
[
|
||||
'sortable' => true,
|
||||
'title' => 'datetime',
|
||||
'field' => 'datetime',
|
||||
'sortable' => true,
|
||||
'filterControl' => 'input',
|
||||
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
$sql = "SELECT
|
||||
req.rowid AS id,
|
||||
ip.data AS ip,
|
||||
domain.data AS domain,
|
||||
path.data AS path,
|
||||
useragent.data AS useragent,
|
||||
headers.data AS headers ,
|
||||
(SELECT load.load1
|
||||
FROM load
|
||||
WHERE load.rowid >= req.timestamp
|
||||
ORDER BY load.rowid DESC LIMIT 1) AS load,
|
||||
datetime(req.timestamp, 'auto') AS datetime
|
||||
|
||||
FROM
|
||||
request req
|
||||
LEFT JOIN
|
||||
ip ON req.id_ip = ip.rowid
|
||||
LEFT JOIN
|
||||
domain ON req.id_domain = domain.rowid
|
||||
LEFT JOIN
|
||||
path ON req.id_path = path.rowid
|
||||
LEFT JOIN
|
||||
useragent ON req.id_useragent = useragent.rowid
|
||||
LEFT JOIN
|
||||
headers ON req.id_headers = headers.rowid
|
||||
WHERE 1=1 ";
|
||||
|
||||
|
||||
$params = [];
|
||||
$query = $request->getQueryParams();
|
||||
if (isset($query['filter'])) {
|
||||
$filter = json_decode($request->getQueryParams()['filter'], true);
|
||||
} else {
|
||||
$filter = [];
|
||||
}
|
||||
|
||||
foreach ($filter as $field => $value) {
|
||||
$sql .= 'AND ' . $field . ' LIKE ? ';
|
||||
$params[] = '%' . $value . '%';
|
||||
}
|
||||
$sql .= " AND req.timestamp BETWEEN ? AND ? ";
|
||||
$sql .= ' ORDER BY req.rowid DESC ';
|
||||
$sql .= ' LIMIT ? ;';
|
||||
$params[] = strtotime($request->getQueryParams()['from'] ?? 'yesterday');
|
||||
$params[] = strtotime($request->getQueryParams()['to'] ?? 'now');
|
||||
$params[] = (int)$request->getQueryParams()['limit'] ?? 100;
|
||||
|
||||
return \XBotControl\Storage::getInstance()->db->query($sql, $params)->then(function ($result) use ($columnsDefinition) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => $result->rows,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
62
src/Classes/Schedule.php
Normal file
62
src/Classes/Schedule.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
/**
|
||||
* Class Schedule
|
||||
*
|
||||
* Manages the scheduling and execution of periodic tasks, ensuring tasks with the same interval
|
||||
* are distributed to avoid simultaneous execution.
|
||||
*/
|
||||
class Schedule
|
||||
{
|
||||
/**
|
||||
* A predefined schedule of tasks to be executed.
|
||||
*
|
||||
* @const array SCHEDULE
|
||||
* - Each item is an array with:
|
||||
* - 'interval': Interval time in seconds.
|
||||
* - 'task': The callable to execute.
|
||||
*/
|
||||
private const SCHEDULE = [
|
||||
['interval' => 60, 'task' => [\XBotControl\Classes\LoadStat::class, 'saveLoad1']],
|
||||
|
||||
];
|
||||
|
||||
/**
|
||||
* Initializes and runs the task scheduler.
|
||||
*
|
||||
* Loops through the SCHEDULE array, setting up periodic timers
|
||||
* using React's event loop. If multiple tasks share the same interval,
|
||||
* they are distributed by adding evenly spaced offsets to avoid collisions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run(): void
|
||||
{
|
||||
$tasksByInterval = [];
|
||||
|
||||
// Group tasks by interval
|
||||
foreach (self::SCHEDULE as $schedule) {
|
||||
$interval = $schedule['interval'];
|
||||
$tasksByInterval[$interval][] = $schedule['task'];
|
||||
}
|
||||
|
||||
// Schedule tasks for each interval
|
||||
foreach ($tasksByInterval as $interval => $tasks) {
|
||||
$taskCount = count($tasks);
|
||||
foreach ($tasks as $index => $task) {
|
||||
// Distribute tasks evenly within the interval
|
||||
$offset = ($index / $taskCount) * $interval;
|
||||
|
||||
Loop::addTimer($offset, function () use ($interval, $task) {
|
||||
Loop::addPeriodicTimer($interval, fn() => call_user_func($task));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Controllers/APIController.php
Normal file
28
src/Controllers/APIController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Controllers;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class APIController
|
||||
{
|
||||
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
|
||||
switch ($request->getAttribute('action')) {
|
||||
case 'report':
|
||||
return call_user_func([\XBotControl\Classes\Report::class, $request->getAttribute('resource')], $request)->then(function ($result) {
|
||||
return \React\Http\Message\Response::json($result);
|
||||
});
|
||||
default:
|
||||
return \React\Http\Message\Response::json(
|
||||
['empty_response']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Controllers/IndexController.php
Normal file
25
src/Controllers/IndexController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Controllers;
|
||||
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): \React\Http\Message\Response
|
||||
{
|
||||
$smarty = \XBotControl\Config::getInstance()->smarty;
|
||||
$smarty->assign([
|
||||
|
||||
]);
|
||||
|
||||
return \React\Http\Message\Response::html(
|
||||
$smarty->fetch('index.tpl')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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) {
|
||||
@@ -28,26 +29,12 @@ class InitTables
|
||||
})->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 ;');
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS settings ( key TEXT UNIQUE NOT NULL, value TEXT 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 ;');
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS load (load1 REAL NOT NULL) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('PRAGMA journal_mode=WAL;');
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
33
src/Instances/Whitelist.php
Normal file
33
src/Instances/Whitelist.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl;
|
||||
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
|
||||
class Whitelist
|
||||
{
|
||||
private static ?Whitelist $instance = null;
|
||||
|
||||
public $googleUrls = [
|
||||
'https://developers.google.com/static/search/apis/ipranges/googlebot.json',
|
||||
'https://developers.google.com/static/search/apis/ipranges/special-crawlers.json',
|
||||
'https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers.json',
|
||||
'https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers-google.json'
|
||||
];
|
||||
|
||||
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function getInstance(): Whitelist
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Promise;
|
||||
use function React\Promise\Timer\sleep;
|
||||
|
||||
class Request
|
||||
|
||||
@@ -31,33 +32,40 @@ class Request
|
||||
|
||||
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();
|
||||
$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)),
|
||||
];
|
||||
// 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', '/' . $request->getAttribute('original_uri', '')),
|
||||
'id_useragent' => $storage::getId('useragent', $userAgent),
|
||||
'id_headers' => 0,
|
||||
];
|
||||
|
||||
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();
|
||||
if ($_ENV['SAVE_HEADERS'] === true) {
|
||||
$idPromises['id_headers'] = $storage::getId('headers', $headers);
|
||||
}
|
||||
|
||||
// Directly save data asynchronously
|
||||
return $storage::insert('request', $resolvedValues);
|
||||
})
|
||||
->then(function () {
|
||||
return \React\Http\Message\Response::plaintext('');
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -10,8 +10,10 @@ use Clue\React\SQLite\Result;
|
||||
|
||||
class Storage
|
||||
{
|
||||
protected static $instance;
|
||||
private static ?Storage $instance = null;
|
||||
|
||||
/** @var DatabaseInterface $db */
|
||||
|
||||
public $db;
|
||||
public $cache = [];
|
||||
|
||||
@@ -23,7 +25,7 @@ class Storage
|
||||
'path'
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
private function __construct()
|
||||
{
|
||||
$this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/requests.sqlite3');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user