modified: xbotcontrol.php

This commit is contained in:
2024-12-17 15:50:30 +02:00
parent 03fd8a7df7
commit 141deaa35b
15 changed files with 1042 additions and 60 deletions

25
src/Classes/LoadStat.php Normal file
View 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
View 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
View 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));
});
}
}
}
}

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

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

View File

@@ -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;');
});
}
}

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

View File

@@ -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

View File

@@ -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');