243 lines
8.2 KiB
PHP
243 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace Jakach\Logging\Api;
|
|
|
|
use Jakach\Logging\Model\{LogSourceType, AlertStatus};
|
|
use Jakach\Logging\Storage\{Database, Repository};
|
|
use Jakach\Logging\RuleEngine\Engine;
|
|
|
|
class Router
|
|
{
|
|
private Repository $repo;
|
|
private AuthMiddleware $auth;
|
|
private Engine $engine;
|
|
|
|
public function __construct()
|
|
{
|
|
$db = new Database();
|
|
$this->repo = new Repository($db);
|
|
$this->auth = new AuthMiddleware($this->repo);
|
|
$this->engine = new Engine($this->repo);
|
|
}
|
|
|
|
public function handle(): void
|
|
{
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
$path = rtrim($path, '/');
|
|
$path = $path ?: '/';
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
try {
|
|
$publicPaths = ['/health', '/oauth', '/auth/me', '/auth/logout', '/ingest'];
|
|
$isPublic = false;
|
|
foreach ($publicPaths as $pp) {
|
|
if ($path === $pp || str_starts_with($path, $pp . '/')) {
|
|
$isPublic = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$isPublic) {
|
|
$user = $this->auth->requireAuth();
|
|
if (!$user) {
|
|
http_response_code(401);
|
|
echo json_encode(['error' => 'Unauthorized', 'login_url' => $this->loginUrl()]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
$result = match (true) {
|
|
$path === '/health' && $method === 'GET' => ['status' => 'ok'],
|
|
|
|
$path === '/ingest' && $method === 'POST' => $this->ingest(),
|
|
|
|
$path === '/auth/me' && $method === 'GET' => $this->getMe(),
|
|
$path === '/auth/logout' && $method === 'POST' => $this->logout(),
|
|
|
|
$path === '/sources' && $method === 'GET' => $this->repo->getSources(),
|
|
$path === '/sources' && $method === 'POST' => $this->createSource(),
|
|
preg_match('#^/sources/(\d+)$#', $path, $m) && $method === 'DELETE'
|
|
=> $this->deleteEntity('source', (int) $m[1]),
|
|
|
|
$path === '/rules' && $method === 'GET' => $this->repo->getRules(),
|
|
$path === '/rules' && $method === 'POST' => $this->createRule(),
|
|
preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'DELETE'
|
|
=> $this->deleteEntity('rule', (int) $m[1]),
|
|
|
|
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
|
|
$path === '/alerts/search' && $method === 'GET' => $this->searchAlerts(),
|
|
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST'
|
|
=> $this->ackAlert((int) $m[1]),
|
|
preg_match('#^/alerts/counts$#', $path) && $method === 'GET'
|
|
=> $this->repo->getAlertCounts(),
|
|
|
|
$path === '/logs/search' && $method === 'GET' => $this->searchLogs(),
|
|
|
|
$path === '/config/allowed_tokens' && $method === 'GET'
|
|
=> ['tokens' => $this->repo->getAllowedUserTokens()],
|
|
$path === '/config/allowed_tokens' && $method === 'PUT'
|
|
=> $this->updateAllowedTokens(),
|
|
|
|
default => throw new \RuntimeException('Not found', 404),
|
|
};
|
|
|
|
$this->respond(200, $result);
|
|
|
|
} catch (\RuntimeException $e) {
|
|
$this->respond($e->getCode() ?: 500, ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
private function respond(int $code, mixed $result): void
|
|
{
|
|
http_response_code($code);
|
|
|
|
if (is_array($result)) {
|
|
$hasObjects = false;
|
|
$isList = array_is_list($result);
|
|
foreach ($result as $key => $val) {
|
|
if (is_object($val) && method_exists($val, 'toArray')) {
|
|
$result[$key] = $val->toArray();
|
|
$hasObjects = true;
|
|
}
|
|
}
|
|
if ($hasObjects || ($isList && (empty($result) || !isset($result['data'])))) {
|
|
if ($isList) {
|
|
$result = ['data' => $result];
|
|
}
|
|
}
|
|
} elseif (is_object($result) && method_exists($result, 'toArray')) {
|
|
$result = ['data' => $result->toArray()];
|
|
}
|
|
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
private function loginUrl(): string
|
|
{
|
|
$redirect = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/oauth.php';
|
|
return 'https://auth.jakach.ch/?send_to=' . urlencode($redirect);
|
|
}
|
|
|
|
private function getMe(): array
|
|
{
|
|
$user = $this->auth->requireAuth();
|
|
if (!$user) {
|
|
http_response_code(401);
|
|
return ['error' => 'Not logged in', 'login_url' => $this->loginUrl()];
|
|
}
|
|
return ['user' => $user];
|
|
}
|
|
|
|
private function logout(): array
|
|
{
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
$_SESSION = [];
|
|
if (ini_get("session.use_cookies")) {
|
|
$params = session_get_cookie_params();
|
|
setcookie(session_name(), '', time() - 42000,
|
|
$params["path"], $params["domain"],
|
|
$params["secure"], $params["httponly"]
|
|
);
|
|
}
|
|
session_destroy();
|
|
return ['status' => 'logged_out'];
|
|
}
|
|
|
|
private function createSource(): mixed
|
|
{
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
$type = LogSourceType::from($body['type'] ?? '');
|
|
return $this->repo->createSource(
|
|
name: $body['name'],
|
|
type: $type,
|
|
address: $body['address'],
|
|
labels: $body['labels'] ?? [],
|
|
);
|
|
}
|
|
|
|
private function createRule(): mixed
|
|
{
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
return $this->repo->createRule(
|
|
name: $body['name'],
|
|
pattern: $body['pattern'],
|
|
severity: $body['severity'] ?? 'warning',
|
|
rateLimitSeconds: $body['rate_limit_seconds'] ?? null,
|
|
);
|
|
}
|
|
|
|
private function deleteEntity(string $type, int $id): array
|
|
{
|
|
if ($type === 'source') {
|
|
$this->repo->deleteSource($id);
|
|
} else {
|
|
$this->repo->deleteRule($id);
|
|
}
|
|
return ['status' => 'deleted', 'id' => $id];
|
|
}
|
|
|
|
private function getAlerts(): mixed
|
|
{
|
|
$limit = (int) ($_GET['limit'] ?? 100);
|
|
$offset = (int) ($_GET['offset'] ?? 0);
|
|
$status = $_GET['status'] ?? null;
|
|
$severity = $_GET['severity'] ?? null;
|
|
return $this->repo->getAlerts($limit, $offset, $status, $severity);
|
|
}
|
|
|
|
private function ackAlert(int $id): array
|
|
{
|
|
$this->repo->updateAlertStatus($id, AlertStatus::Acknowledged);
|
|
return ['status' => 'acknowledged', 'id' => $id];
|
|
}
|
|
|
|
private function searchAlerts(): array
|
|
{
|
|
$query = $_GET['q'] ?? '';
|
|
if (empty($query)) {
|
|
return [];
|
|
}
|
|
$limit = (int) ($_GET['limit'] ?? 100);
|
|
return $this->repo->searchAlerts($query, $limit);
|
|
}
|
|
|
|
private function searchLogs(): array
|
|
{
|
|
$query = $_GET['q'] ?? '';
|
|
if (empty($query)) {
|
|
return ['data' => []];
|
|
}
|
|
$limit = (int) ($_GET['limit'] ?? 200);
|
|
$offset = (int) ($_GET['offset'] ?? 0);
|
|
return ['data' => $this->repo->searchLogEntries($query, $limit, $offset)];
|
|
}
|
|
|
|
private function updateAllowedTokens(): array
|
|
{
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
$tokens = $body['tokens'] ?? [];
|
|
$this->repo->setAllowedUserTokens($tokens);
|
|
return ['status' => 'saved', 'tokens' => $this->repo->getAllowedUserTokens()];
|
|
}
|
|
|
|
private function ingest(): array
|
|
{
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
$line = $body['line'] ?? '';
|
|
$source = $body['source'] ?? 'http';
|
|
|
|
if (empty($line)) {
|
|
http_response_code(400);
|
|
return ['error' => 'Missing "line" field'];
|
|
}
|
|
|
|
$this->engine->evaluate($line, null);
|
|
|
|
return ['status' => 'ingested', 'line' => substr($line, 0, 100)];
|
|
}
|
|
} |