Initial commit

This commit is contained in:
Rekryt
2024-08-30 15:22:24 +03:00
commit 171e449744
46 changed files with 4638 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
<?php
namespace OpenCCK\Infrastructure\API;
use Amp\ByteStream\WritableResourceStream;
use Amp\Log\ConsoleFormatter;
use Amp\Log\StreamHandler;
use Revolt\EventLoop;
use Revolt\EventLoop\UnsupportedFeatureException;
use Closure;
use Dotenv\Dotenv;
use Monolog\Logger;
use Psr\Log\LogLevel;
use function Amp\trapSignal;
use function OpenCCK\getEnv;
use function sprintf;
final class App {
private static App $_instance;
/**
* @param array<AppModuleInterface> $modules
*/
private array $modules = [];
private bool $isEventLoopStarted = false;
/**
* @param ?Logger $logger
*/
private function __construct(private ?Logger $logger = null) {
ini_set('memory_limit', getEnv('SYS_MEMORY_LIMIT') ?? '2048M');
if (!defined('PATH_ROOT')) {
define('PATH_ROOT', dirname(__DIR__, 3));
}
$dotenv = Dotenv::createImmutable(PATH_ROOT);
$dotenv->safeLoad();
if ($timezone = getEnv('SYS_TIMEZONE')) {
date_default_timezone_set($timezone);
}
$this->logger = $logger ?? new Logger(getEnv('COMPOSE_PROJECT_NAME') ?? 'iplist');
$logHandler = new StreamHandler(new WritableResourceStream(STDOUT));
$logHandler->setFormatter(new ConsoleFormatter());
$logHandler->setLevel(getEnv('DEBUG') === 'false' ? LogLevel::INFO : LogLevel::DEBUG);
$this->logger->pushHandler($logHandler);
EventLoop::setErrorHandler(function ($e) {
$this->logger->error($e->getMessage());
});
}
public static function getInstance(?Logger $logger = null): self {
return self::$_instance ??= new self($logger);
}
/**
* @param Closure<AppModuleInterface> $handler
* @return $this
*/
public function addModule(Closure $handler): self {
$module = $handler($this);
$this->modules[$module::class] = $module;
return $this;
}
public function getModules(): array {
return $this->modules;
}
public static function getLogger(): ?Logger {
return self::$_instance->logger;
}
public function start(): void {
foreach ($this->getModules() as $module) {
$module->start();
}
if (defined('SIGINT') && defined('SIGTERM')) {
// Await SIGINT or SIGTERM to be received.
try {
$signal = trapSignal([SIGINT, SIGTERM]);
$this->logger->info(sprintf('Received signal %d, stopping server', $signal));
} catch (UnsupportedFeatureException $e) {
$this->logger->error($e->getMessage());
}
$this->stop();
} else {
if (!$this->isEventLoopStarted && !defined('PHPUNIT_COMPOSER_INSTALL')) {
$this->isEventLoopStarted = true;
EventLoop::run();
}
}
}
public function stop(): void {
foreach ($this->modules as $module) {
$module->stop();
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace OpenCCK\Infrastructure\API;
interface AppModuleInterface {
public function start(): void;
public function stop(): void;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace OpenCCK\Infrastructure\API\Handler;
use Amp\Http\Server\ErrorHandler;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
final class HTTPErrorHandler implements ErrorHandler {
public function handleError(int $status, ?string $reason = null, ?Request $request = null): Response {
return new Response(
status: $status,
headers: ['content-type' => 'application/json; charset=utf-8'],
body: json_encode(['message' => $reason, 'code' => $status])
);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace OpenCCK\Infrastructure\API\Handler;
use Amp\Http\Server\Request;
use Amp\Http\Server\RequestHandler;
use Amp\Http\Server\RequestHandler\ClosureRequestHandler;
use Amp\Http\Server\Response;
use Psr\Log\LoggerInterface;
use Throwable;
use function OpenCCK\getEnv;
final class HTTPHandler extends Handler implements HTTPHandlerInterface {
private function __construct(private readonly LoggerInterface $logger, array $headers = null) {
}
public static function getInstance(LoggerInterface $logger, array $headers = null): HTTPHandler {
return new self($logger, $headers);
}
/**
* @param string $controllerName
* @return RequestHandler
*/
public function getHandler(string $controllerName = 'main'): RequestHandler {
return new ClosureRequestHandler(function (Request $request) use ($controllerName): Response {
try {
$response = $this->getController(
ucfirst($request->getQueryParameter('format') ?: $controllerName),
$request,
$this->headers ?? []
)();
} catch (Throwable $e) {
$this->logger->warning('Exception', [
'exception' => $e::class,
'error' => $e->getMessage(),
'code' => $e->getCode(),
]);
$response = new Response(
status: $e->getCode() ?: 500,
headers: $this->headers ?? ['content-type' => 'application/json; charset=utf-8'],
body: json_encode(
array_merge(
['message' => $e->getMessage(), 'code' => $e->getCode()],
getEnv('DEBUG') === 'true'
? ['file' => $e->getFile() . ':' . $e->getLine(), 'trace' => $e->getTrace()]
: []
)
)
);
}
return $response;
});
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace OpenCCK\Infrastructure\API\Handler;
use Amp\Http\Server\RequestHandler;
interface HTTPHandlerInterface extends HandlerInterface {
/**
* @return RequestHandler
*/
public function getHandler(): RequestHandler;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace OpenCCK\Infrastructure\API\Handler;
use Amp\Http\HttpStatus;
use Amp\Http\Server\Request;
use OpenCCK\App\Controller\AbstractController;
use Exception;
abstract class Handler implements HandlerInterface {
/**
* @param string $name
* @param Request $request
* @param ?string[] $headers
* @return AbstractController
* @throws Exception
*/
protected function getController(string $name, Request $request, array $headers = null): AbstractController {
$className = '\\OpenCCK\\App\\Controller\\' . ucfirst($name) . 'Controller';
if (!class_exists($className)) {
throw new Exception('Controller ' . $className . ' not found', HttpStatus::NOT_FOUND);
}
return new $className($request, $headers ?? ['content-type' => 'text/plain']);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace OpenCCK\Infrastructure\API\Handler;
use Amp\Http\Server\RequestHandler;
interface HandlerInterface {
/**
* @return RequestHandler
*/
public function getHandler(): RequestHandler;
}

View File

@@ -0,0 +1,119 @@
<?php
namespace OpenCCK\Infrastructure\API;
use Amp\ByteStream\BufferException;
use Amp\Http\Server\DefaultErrorHandler;
use Amp\Http\Server\Driver\ConnectionLimitingClientFactory;
use Amp\Http\Server\Driver\ConnectionLimitingServerSocketFactory;
use Amp\Http\Server\Driver\DefaultHttpDriverFactory;
use Amp\Http\Server\Driver\SocketClientFactory;
use Amp\Http\Server\ErrorHandler;
use Amp\Http\Server\HttpServer;
use Amp\Http\Server\HttpServerStatus;
use Amp\Http\Server\SocketHttpServer;
use Amp\Socket\BindContext;
use Amp\Sync\LocalSemaphore;
use Amp\Socket;
use Monolog\Logger;
use OpenCCK\App\Service\IPListService;
use OpenCCK\Infrastructure\API\Handler\HTTPHandler;
use Throwable;
use function OpenCCK\getEnv;
final class Server implements AppModuleInterface {
private static Server $_instance;
private int $connectionLimit = 1024;
private int $connectionPerIpLimit = 10;
/**
* @param ?HttpServer $httpServer
* @param ?ErrorHandler $errorHandler
* @param ?BindContext $bindContext
* @param ?Logger $logger
* @throws Throwable
*/
private function __construct(
private ?HttpServer $httpServer,
private ?ErrorHandler $errorHandler,
private ?Socket\BindContext $bindContext,
private ?Logger $logger
) {
$this->logger = $logger ?? App::getLogger();
$serverSocketFactory = new ConnectionLimitingServerSocketFactory(new LocalSemaphore($this->connectionLimit));
$clientFactory = new ConnectionLimitingClientFactory(
new SocketClientFactory($this->logger),
$this->logger,
$this->connectionPerIpLimit
);
$this->httpServer =
$httpServer ??
new SocketHttpServer(
logger: $this->logger,
serverSocketFactory: $serverSocketFactory,
clientFactory: $clientFactory,
httpDriverFactory: new DefaultHttpDriverFactory(logger: $this->logger, streamTimeout: 60)
);
$this->bindContext = $bindContext ?? (new Socket\BindContext())->withoutTlsContext();
$this->errorHandler = $errorHandler ?? new DefaultErrorHandler();
// инициализация сервиса
IPListService::getInstance($this->logger);
}
/**
* @param ?HttpServer $httpServer
* @param ?ErrorHandler $errorHandler
* @param ?BindContext $bindContext
* @param ?Logger $logger
* @throws BufferException
* @throws Throwable
*/
public static function getInstance(
HttpServer $httpServer = null,
ErrorHandler $errorHandler = null,
Socket\BindContext $bindContext = null,
Logger $logger = null
): Server {
return self::$_instance ??= new self($httpServer, $errorHandler, $bindContext, $logger);
}
/**
* Запуск веб-сервера
* @return void
*/
public function start(): void {
try {
$this->httpServer->expose(
new Socket\InternetAddress(getEnv('HTTP_HOST') ?? '0.0.0.0', getEnv('HTTP_PORT') ?? 8080),
$this->bindContext
);
//$this->socketHttpServer->expose(
// new Socket\InternetAddress('[::]', $_ENV['HTTP_PORT'] ?? 8080),
// $this->bindContext
//);
$this->httpServer->start(HTTPHandler::getInstance($this->logger)->getHandler(), $this->errorHandler);
} catch (Socket\SocketException $e) {
$this->logger->warning($e->getMessage());
} catch (Throwable $e) {
$this->logger->error($e->getMessage());
}
}
/**
* @return void
*/
public function stop(): void {
$this->httpServer->stop();
}
/**
* @return HttpServerStatus
*/
public function getStatus(): HttpServerStatus {
return $this->httpServer->getStatus();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace OpenCCK\Infrastructure\Storage;
use OpenCCK\Infrastructure\API\App;
use Revolt\EventLoop;
class CIDRStorage implements StorageInterface {
const FILENAME = 'cidr.json';
private static CIDRStorage $_instance;
private array $data = [];
private function __construct() {
$path = PATH_ROOT . '/storage/' . self::FILENAME;
if (is_file($path)) {
$this->data = (array) json_decode(file_get_contents($path)) ?? [];
}
EventLoop::repeat(\OpenCCK\getEnv('STORAGE_SAVE_INTERVAL') ?? 120, $this->save(...));
}
public static function getInstance(): CIDRStorage {
return self::$_instance ??= new self();
}
public function get(string $key): ?array {
return $this->data[$key] ?? null;
}
public function set(string $key, mixed $value): bool {
$this->data[$key] = $value;
return true;
}
public function has(string $key): bool {
return isset($this->data[$key]);
}
private function save(): void {
file_put_contents(PATH_ROOT . '/storage/' . self::FILENAME, json_encode($this->data, JSON_PRETTY_PRINT));
App::getLogger()->notice('Whois storage saved', [count($this->data) . ' items']);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace OpenCCK\Infrastructure\Storage;
interface StorageInterface {
public function get(string $key): mixed;
public function set(string $key, mixed $value): bool;
public function has(string $key): bool;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace OpenCCK\Infrastructure\Task;
use Amp\Cancellation;
use Amp\Sync\Channel;
readonly class ShellTask implements TaskInterface {
public function __construct(private string $command) {
}
/**
* @param Channel $channel
* @param Cancellation $cancellation
* @return string
*/
public function run(Channel $channel, Cancellation $cancellation): mixed {
return shell_exec($this->command);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace OpenCCK\Infrastructure\Task;
use Amp\Cancellation;
use Amp\Parallel\Worker\Task;
use Amp\Sync\Channel;
interface TaskInterface extends Task {
public function run(Channel $channel, Cancellation $cancellation): mixed;
}