mirror of
https://github.com/rekryt/iplist.git
synced 2025-10-12 16:39:35 +03:00
Initial commit
This commit is contained in:
60
src/App/Controller/AbstractController.php
Normal file
60
src/App/Controller/AbstractController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
use Amp\Http\HttpStatus;
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\Response;
|
||||
|
||||
use Throwable;
|
||||
|
||||
abstract class AbstractController implements ControllerInterface {
|
||||
private int $httpStatus = HttpStatus::OK;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $headers
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(protected Request $request, protected array $headers = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(): Response {
|
||||
return new Response(status: $this->httpStatus, headers: $this->headers, body: $this->getBody());
|
||||
}
|
||||
|
||||
abstract public function getBody(): string;
|
||||
|
||||
public function setHeaders(array $headers): AbstractController {
|
||||
$this->headers = $headers;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $httpStatus
|
||||
*/
|
||||
public function setHttpStatus(int $httpStatus): void {
|
||||
$this->httpStatus = $httpStatus;
|
||||
}
|
||||
|
||||
public function redirect(string $url, bool $permanently = false): void {
|
||||
$this->httpStatus = $permanently ? HttpStatus::MOVED_PERMANENTLY : HttpStatus::SEE_OTHER;
|
||||
$this->headers = array_merge($this->headers ?? [], ['location' => $url]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseURL(): string {
|
||||
$schemePort = ['http' => 80, 'https' => 443];
|
||||
return $this->request->getUri()->getScheme() .
|
||||
'://' .
|
||||
$this->request->getUri()->getHost() .
|
||||
($schemePort[$this->request->getUri()->getScheme()] !== $this->request->getUri()->getPort()
|
||||
? ':' . $this->request->getUri()->getPort()
|
||||
: '');
|
||||
}
|
||||
}
|
35
src/App/Controller/AbstractIPListController.php
Normal file
35
src/App/Controller/AbstractIPListController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
use Amp\ByteStream\BufferException;
|
||||
use Amp\Http\Server\Request;
|
||||
|
||||
use OpenCCK\App\Service\IPListService;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Throwable;
|
||||
|
||||
abstract class AbstractIPListController extends AbstractController {
|
||||
protected Logger $logger;
|
||||
protected IPListService $service;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $headers
|
||||
* @throws BufferException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(protected Request $request, protected array $headers = []) {
|
||||
parent::__construct($request, $this->headers);
|
||||
|
||||
$this->logger = App::getLogger();
|
||||
$this->service = IPListService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getBody(): string;
|
||||
}
|
19
src/App/Controller/ControllerInterface.php
Normal file
19
src/App/Controller/ControllerInterface.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\Response;
|
||||
|
||||
interface ControllerInterface {
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $headers
|
||||
*/
|
||||
public function __construct(Request $request, array $headers = []);
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(): Response;
|
||||
}
|
32
src/App/Controller/JsonController.php
Normal file
32
src/App/Controller/JsonController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class JsonController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'application/json']);
|
||||
|
||||
$site = $this->request->getQueryParameter('site') ?? '';
|
||||
$data = $this->request->getQueryParameter('data') ?? '';
|
||||
if ($site == '') {
|
||||
if ($data == '') {
|
||||
return json_encode($this->service->sites);
|
||||
} else {
|
||||
$result = [];
|
||||
foreach ($this->service->sites as $site) {
|
||||
$result[$site->name] = $site->$data;
|
||||
}
|
||||
return json_encode($result);
|
||||
}
|
||||
} else {
|
||||
if ($data == '') {
|
||||
return json_encode($this->service->sites[$site]);
|
||||
} else {
|
||||
return json_encode($this->service->sites[$site]->$data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
src/App/Controller/MainController.php
Normal file
23
src/App/Controller/MainController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class MainController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'text/html; charset=utf-8']);
|
||||
return $this->renderTemplate('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @return string
|
||||
*/
|
||||
private function renderTemplate(string $template): string {
|
||||
ob_start();
|
||||
include PATH_ROOT . '/src/App/Template/' . ucfirst($template) . 'Template.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
41
src/App/Controller/MikrotikController.php
Normal file
41
src/App/Controller/MikrotikController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class MikrotikController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'text/plain']);
|
||||
|
||||
$site = $this->request->getQueryParameter('site') ?? '';
|
||||
$data = $this->request->getQueryParameter('data') ?? '';
|
||||
if ($data == '') {
|
||||
return "# Error: The 'data' GET parameter is required in the URL to access this page, but it cannot have the value 'All'";
|
||||
}
|
||||
$response = '/ip firewall address-list' . "\n";
|
||||
if ($site == '') {
|
||||
foreach ($this->service->sites as $site) {
|
||||
$response .= $this->render($site->name, $site->$data);
|
||||
}
|
||||
return $response;
|
||||
} else {
|
||||
return $response . $this->render($site, $this->service->sites[$site]->$data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $site
|
||||
* @param iterable $array
|
||||
* @return string
|
||||
*/
|
||||
private function render(string $site, iterable $array): string {
|
||||
$response = '';
|
||||
$listName = str_replace(' ', '', $site);
|
||||
foreach ($array as $item) {
|
||||
$response .= 'add list=' . $listName . ' address=' . $item . ' comment=' . $listName . "\n";
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
36
src/App/Controller/TextController.php
Normal file
36
src/App/Controller/TextController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class TextController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'text/plain']);
|
||||
|
||||
$site = $this->request->getQueryParameter('site') ?? '';
|
||||
$data = $this->request->getQueryParameter('data') ?? '';
|
||||
if ($data == '') {
|
||||
return "# Error: The 'data' GET parameter is required in the URL to access this page, but it cannot have the value 'All'";
|
||||
}
|
||||
|
||||
$response = '';
|
||||
if ($site == '') {
|
||||
foreach ($this->service->sites as $site) {
|
||||
$response .= $this->render($site->name, $site->$data);
|
||||
}
|
||||
return $response;
|
||||
} else {
|
||||
return $this->render($site, $this->service->sites[$site]->$data);
|
||||
}
|
||||
}
|
||||
|
||||
private function render(string $name, iterable $array): string {
|
||||
$response = '# ' . $name . ' ' . date('Y-m-d H:i:s') . "\n";
|
||||
foreach ($array as $item) {
|
||||
$response .= $item . "\n";
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
50
src/App/Service/IPListService.php
Normal file
50
src/App/Service/IPListService.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\App\Service;
|
||||
|
||||
use OpenCCK\Domain\Entity\Site;
|
||||
use OpenCCK\Domain\Factory\SiteFactory;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Exception;
|
||||
use Monolog\Logger;
|
||||
|
||||
class IPListService {
|
||||
private static IPListService $_instance;
|
||||
private ?Logger $logger;
|
||||
|
||||
/**
|
||||
* @var array<string, Site>
|
||||
*/
|
||||
public array $sites = [];
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function __construct(Logger $logger = null) {
|
||||
$this->logger = $logger ?? App::getLogger();
|
||||
|
||||
$dir = PATH_ROOT . '/config/';
|
||||
if (!is_dir($dir)) {
|
||||
throw new Exception('config directory not found');
|
||||
}
|
||||
foreach (scandir($dir) as $file) {
|
||||
if (str_ends_with($file, '.json')) {
|
||||
$this->loadConfig(substr($file, 0, -5), json_decode(file_get_contents($dir . $file)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?Logger $logger
|
||||
* @return IPListService
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getInstance(Logger $logger = null): IPListService {
|
||||
return self::$_instance ??= new self($logger);
|
||||
}
|
||||
|
||||
private function loadConfig(string $name, object $config): void {
|
||||
$this->sites[$name] = SiteFactory::create($name, $config);
|
||||
}
|
||||
}
|
588
src/App/Template/IndexTemplate.php
Normal file
588
src/App/Template/IndexTemplate.php
Normal file
@@ -0,0 +1,588 @@
|
||||
<?php
|
||||
use OpenCCK\App\Controller\TextController;
|
||||
/** @var TextController $this */
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>IPList</title>
|
||||
<style>
|
||||
/* MVP.css v1.15 - https://github.com/andybrewer/mvp */
|
||||
:root {
|
||||
--active-brightness: 0.85;
|
||||
--border-radius: 5px;
|
||||
--box-shadow: 2px 2px 10px;
|
||||
--color-accent: #118bee15;
|
||||
--color-bg: #fff;
|
||||
--color-bg-secondary: #e9e9e9;
|
||||
--color-link: #118bee;
|
||||
--color-secondary: #920de9;
|
||||
--color-secondary-accent: #920de90b;
|
||||
--color-shadow: #f4f4f4;
|
||||
--color-table: #118bee;
|
||||
--color-text: #000;
|
||||
--color-text-secondary: #999;
|
||||
--color-scrollbar: #cacae8;
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
--hover-brightness: 1.2;
|
||||
--justify-important: center;
|
||||
--justify-normal: left;
|
||||
--line-height: 1.5;
|
||||
--width-card: 285px;
|
||||
--width-card-medium: 460px;
|
||||
--width-card-wide: 800px;
|
||||
--width-content: 1080px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[color-mode="user"] {
|
||||
--color-accent: #0097fc4f;
|
||||
--color-bg: #333;
|
||||
--color-bg-secondary: #555;
|
||||
--color-link: #0097fc;
|
||||
--color-secondary: #e20de9;
|
||||
--color-secondary-accent: #e20de94f;
|
||||
--color-shadow: #bbbbbb20;
|
||||
--color-table: #0097fc;
|
||||
--color-text: #f7f7f7;
|
||||
--color-text-secondary: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
article aside {
|
||||
background: var(--color-secondary-accent);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
padding: 0.01rem 0.8rem;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
line-height: var(--line-height);
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer,
|
||||
header,
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: var(--width-content);
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 4rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: var(--justify-important);
|
||||
}
|
||||
|
||||
section img,
|
||||
article img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
section pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
section aside {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
margin: 1rem;
|
||||
padding: 1.25rem;
|
||||
width: var(--width-card);
|
||||
}
|
||||
|
||||
section aside:hover {
|
||||
box-shadow: var(--box-shadow) var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
article header,
|
||||
div header,
|
||||
main header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
header a b,
|
||||
header a em,
|
||||
header a i,
|
||||
header a strong {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
header nav img {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
section header {
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
nav {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 7rem;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Nav Dropdown */
|
||||
nav ul li:hover ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav ul li ul {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
display: none;
|
||||
height: auto;
|
||||
left: -2px;
|
||||
padding: .5rem 1rem;
|
||||
position: absolute;
|
||||
top: 1.7rem;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav ul li ul::before {
|
||||
/* fill gap above to make mousing over them easier */
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
nav ul li ul li,
|
||||
nav ul li ul li a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
code,
|
||||
samp {
|
||||
background-color: var(--color-accent);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
display: inline-block;
|
||||
margin: 0 0.1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
details {
|
||||
margin: 1.3rem 0;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
ol li,
|
||||
ul li {
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.75rem 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
max-width: var(--width-card-wide);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
pre code,
|
||||
pre samp {
|
||||
display: block;
|
||||
max-width: var(--width-card-wide);
|
||||
padding: 0.5rem 2rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
sup {
|
||||
background-color: var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-bg);
|
||||
font-size: xx-small;
|
||||
font-weight: bold;
|
||||
margin: 0.2rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--color-link);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
filter: brightness(var(--hover-brightness));
|
||||
}
|
||||
|
||||
a:active {
|
||||
filter: brightness(var(--active-brightness));
|
||||
}
|
||||
|
||||
a b,
|
||||
a em,
|
||||
a i,
|
||||
a strong,
|
||||
button,
|
||||
input[type="submit"] {
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
line-height: var(--line-height);
|
||||
margin: 0.5rem 0;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="submit"] {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
button:hover,
|
||||
input[type="submit"]:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(var(--hover-brightness));
|
||||
}
|
||||
|
||||
button:active,
|
||||
input[type="submit"]:active {
|
||||
filter: brightness(var(--active-brightness));
|
||||
}
|
||||
|
||||
a b,
|
||||
a strong,
|
||||
button,
|
||||
input[type="submit"] {
|
||||
background-color: var(--color-link);
|
||||
border: 2px solid var(--color-link);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
a em,
|
||||
a i {
|
||||
border: 2px solid var(--color-link);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-link);
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
article aside a {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Images */
|
||||
figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
figure img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figure figcaption {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
button:disabled,
|
||||
input:disabled {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button[disabled]:hover,
|
||||
input[type="submit"][disabled]:hover {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
form {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
display: block;
|
||||
max-width: var(--width-card-wide);
|
||||
min-width: var(--width-card);
|
||||
padding: 1.5rem;
|
||||
text-align: var(--justify-normal);
|
||||
}
|
||||
|
||||
form header {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
label,
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
max-width: var(--width-card-wide);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type="checkbox"]+label,
|
||||
input[type="radio"]+label {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
width: calc(100% - 1.6rem);
|
||||
}
|
||||
|
||||
input[readonly],
|
||||
textarea[readonly] {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
/* Popups */
|
||||
dialog {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 50%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
border-spacing: 0;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th,
|
||||
table tr {
|
||||
padding: 0.4rem 0.8rem;
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: var(--color-table);
|
||||
border-collapse: collapse;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-bg);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table thead tr:first-child th:first-child {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
table thead tr:first-child th:last-child {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
table thead th:first-child,
|
||||
table tr td:first-child {
|
||||
text-align: var(--justify-normal);
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Quotes */
|
||||
blockquote {
|
||||
display: block;
|
||||
font-size: x-large;
|
||||
line-height: var(--line-height);
|
||||
margin: 1rem auto;
|
||||
max-width: var(--width-card-medium);
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
font-size: small;
|
||||
line-height: var(--line-height);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-scrollbar) transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar);
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form action="" method="get">
|
||||
<section>
|
||||
<label>
|
||||
Format:
|
||||
<select name="format">
|
||||
<option value="json">JSON</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="mikrotik">MikroTik</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Site:
|
||||
<select name="site">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($this->service->sites as $site): ?>
|
||||
<option value="<?= $site->name ?>"><?= $site->name ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Data:
|
||||
<select name="data">
|
||||
<option value="">All</option>
|
||||
<option value="domains">domains</option>
|
||||
<option value="ip4">ip4</option>
|
||||
<option value="cidr4">cidr4</option>
|
||||
<option value="ip6">ip6</option>
|
||||
<option value="cidr6">cidr6</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
<section>
|
||||
<button type="submit">Submit</button>
|
||||
</section>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
115
src/Domain/Entity/Site.php
Normal file
115
src/Domain/Entity/Site.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Entity;
|
||||
|
||||
use OpenCCK\Domain\Factory\SiteFactory;
|
||||
use OpenCCK\Domain\Helper\DNSHelper;
|
||||
use OpenCCK\Domain\Helper\IP4Helper;
|
||||
use OpenCCK\Domain\Helper\IP6Helper;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Revolt\EventLoop;
|
||||
use stdClass;
|
||||
|
||||
final class Site {
|
||||
private DNSHelper $dnsHelper;
|
||||
|
||||
/**
|
||||
* @param string $name Name of portal
|
||||
* @param array $domains List of portal domains
|
||||
* @param array $dns List of DNS servers for updating IP addresses
|
||||
* @param int $timeout Time interval between domain IP address updates (seconds)
|
||||
* @param array $ip4 List of IPv4 addresses
|
||||
* @param array $ip6 List of IPv6 addresses
|
||||
* @param array $cidr4 List of CIDRv4 zones of IPv4 addresses
|
||||
* @param array $cidr6 List of CIDRv6 zones of IPv6 addresses
|
||||
* @param object $external Lists of URLs to retrieve data from external sources
|
||||
*
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public array $domains = [],
|
||||
public array $dns = [],
|
||||
public int $timeout = 1440 * 60,
|
||||
public array $ip4 = [],
|
||||
public array $ip6 = [],
|
||||
public array $cidr4 = [],
|
||||
public array $cidr6 = [],
|
||||
public object $external = new stdClass()
|
||||
) {
|
||||
$this->dnsHelper = new DNSHelper($dns);
|
||||
EventLoop::delay(0, $this->reload(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function reload(): void {
|
||||
$ip4 = [];
|
||||
$ip6 = [];
|
||||
foreach ($this->domains as $domain) {
|
||||
[$ipv4results, $ipv6results] = $this->dnsHelper->resolve($domain);
|
||||
$ip4 = array_merge($ip4, $ipv4results);
|
||||
$ip6 = array_merge($ip6, $ipv6results);
|
||||
}
|
||||
|
||||
$newIp4 = SiteFactory::normalize(array_diff($ip4, $this->ip4));
|
||||
$this->cidr4 = SiteFactory::normalize(IP4Helper::processCIDR($newIp4, $this->cidr4));
|
||||
|
||||
$newIp6 = SiteFactory::normalize(array_diff($ip6, $this->ip6));
|
||||
$this->cidr6 = SiteFactory::normalize(IP6Helper::processCIDR($newIp6, $this->cidr6));
|
||||
|
||||
$this->ip4 = SiteFactory::normalize(array_merge($this->ip4, $ip4));
|
||||
$this->ip6 = SiteFactory::normalize(array_merge($this->ip6, $ip6));
|
||||
|
||||
App::getLogger()->debug('Reloaded for ' . $this->name);
|
||||
|
||||
EventLoop::delay($this->timeout, function () {
|
||||
$this->reloadExternal();
|
||||
$this->reload();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function reloadExternal(): void {
|
||||
if (isset($this->external->domains)) {
|
||||
foreach ($this->external->domains as $url) {
|
||||
$this->domains = SiteFactory::normalize(
|
||||
array_merge($this->domains, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->ip4)) {
|
||||
foreach ($this->external->ip4 as $url) {
|
||||
$this->ip4 = SiteFactory::normalize(array_merge($this->ip4, explode("\n", file_get_contents($url))));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->ip6)) {
|
||||
foreach ($this->external->ip6 as $url) {
|
||||
$this->ip6 = SiteFactory::normalize(array_merge($this->ip6, explode("\n", file_get_contents($url))));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->cidr4)) {
|
||||
foreach ($this->external->cidr4 as $url) {
|
||||
$this->cidr4 = SiteFactory::normalize(
|
||||
array_merge($this->cidr4, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->cidr6)) {
|
||||
foreach ($this->external->cidr6 as $url) {
|
||||
$this->cidr6 = SiteFactory::normalize(
|
||||
array_merge($this->cidr6, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App::getLogger()->debug('External reloaded for ' . $this->name);
|
||||
}
|
||||
}
|
77
src/Domain/Factory/SiteFactory.php
Normal file
77
src/Domain/Factory/SiteFactory.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Factory;
|
||||
|
||||
use OpenCCK\Domain\Entity\Site;
|
||||
use OpenCCK\Domain\Helper\IP4Helper;
|
||||
use OpenCCK\Domain\Helper\IP6Helper;
|
||||
use stdClass;
|
||||
|
||||
class SiteFactory {
|
||||
/**
|
||||
* @param string $name Name of portal
|
||||
* @param object $config Configuration of portal
|
||||
* @return Site
|
||||
*
|
||||
*/
|
||||
static function create(string $name, object $config): Site {
|
||||
$domains = $config->domains ?? [];
|
||||
$dns = $config->dns ?? [];
|
||||
$timeout = $config->timeout ?? 1440 * 60;
|
||||
$ip4 = $config->ip4 ?? [];
|
||||
$ip6 = $config->ip6 ?? [];
|
||||
$cidr4 = $config->cidr4 ?? [];
|
||||
$cidr6 = $config->cidr6 ?? [];
|
||||
$external = $config->external ?? new stdClass();
|
||||
|
||||
if (isset($external)) {
|
||||
if (isset($external->domains)) {
|
||||
foreach ($external->domains as $url) {
|
||||
$domains = array_merge($domains, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->ip4)) {
|
||||
foreach ($external->ip4 as $url) {
|
||||
$ip4 = array_merge($ip4, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->ip6)) {
|
||||
foreach ($external->ip6 as $url) {
|
||||
$ip6 = array_merge($ip6, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->cidr4)) {
|
||||
foreach ($external->cidr4 as $url) {
|
||||
$cidr4 = array_merge($cidr4, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->cidr6)) {
|
||||
foreach ($external->cidr6 as $url) {
|
||||
$cidr6 = array_merge($cidr6, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$domains = self::normalize($domains);
|
||||
$ip4 = self::normalize($ip4);
|
||||
$ip6 = self::normalize($ip6);
|
||||
$cidr4 = self::normalize(IP4Helper::processCIDR($ip4, self::normalize($cidr4)));
|
||||
$cidr6 = self::normalize(IP6Helper::processCIDR($ip6, self::normalize($cidr6)));
|
||||
|
||||
return new Site($name, $domains, $dns, $timeout, $ip4, $ip6, $cidr4, $cidr6, $external);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public static function normalize(array $array): array {
|
||||
return array_values(
|
||||
array_unique(array_filter($array, fn(string $item) => !str_starts_with($item, '#') && strlen($item) > 0))
|
||||
);
|
||||
}
|
||||
}
|
69
src/Domain/Helper/DNSHelper.php
Normal file
69
src/Domain/Helper/DNSHelper.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use Amp\Dns\DnsConfig;
|
||||
use Amp\Dns\DnsConfigLoader;
|
||||
use Amp\Dns\DnsException;
|
||||
use Amp\Dns\DnsRecord;
|
||||
use Amp\Dns\HostLoader;
|
||||
use Amp\Dns\Rfc1035StubDnsResolver;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use function Amp\Dns\dnsResolver;
|
||||
use function Amp\Dns\resolve;
|
||||
|
||||
readonly class DNSHelper {
|
||||
public function __construct(private array $dnsServers = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $dnsServers
|
||||
* @return void
|
||||
*/
|
||||
private function setResolver(array $dnsServers): void {
|
||||
dnsResolver(
|
||||
new Rfc1035StubDnsResolver(
|
||||
null,
|
||||
new class ($dnsServers) implements DnsConfigLoader {
|
||||
public function __construct(private readonly array $dnsServers = []) {
|
||||
}
|
||||
|
||||
public function loadConfig(): DnsConfig {
|
||||
return new DnsConfig($this->dnsServers, (new HostLoader())->loadHosts());
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @return array[]
|
||||
*/
|
||||
public function resolve(string $domain): array {
|
||||
$ipv4 = [];
|
||||
$ipv6 = [];
|
||||
foreach ($this->dnsServers as $server) {
|
||||
$this->setResolver([$server]);
|
||||
try {
|
||||
$ipv4 = array_merge(
|
||||
$ipv4,
|
||||
array_map(fn(DnsRecord $record) => $record->getValue(), resolve($domain, DnsRecord::A))
|
||||
);
|
||||
} catch (DnsException $e) {
|
||||
App::getLogger()->error($e->getMessage(), [$server]);
|
||||
}
|
||||
try {
|
||||
$ipv6 = array_merge(
|
||||
$ipv6,
|
||||
array_map(fn(DnsRecord $record) => $record->getValue(), resolve($domain, DnsRecord::AAAA))
|
||||
);
|
||||
} catch (DnsException $e) {
|
||||
App::getLogger()->error($e->getMessage(), [$server]);
|
||||
}
|
||||
}
|
||||
App::getLogger()->debug('resolve: ' . $domain, [count($ipv4), count($ipv6)]);
|
||||
return [$ipv4, $ipv6];
|
||||
}
|
||||
}
|
144
src/Domain/Helper/IP4Helper.php
Normal file
144
src/Domain/Helper/IP4Helper.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\Storage\CIDRStorage;
|
||||
|
||||
use function Amp\async;
|
||||
use function Amp\delay;
|
||||
|
||||
class IP4Helper {
|
||||
public static function processCIDR(array $ips, $results = []): array {
|
||||
$count = count($ips);
|
||||
foreach ($ips as $i => $ip) {
|
||||
if ($ip === '127.0.0.1') {
|
||||
continue;
|
||||
}
|
||||
async(function () use ($ip, $i, $count, &$results) {
|
||||
if (CIDRStorage::getInstance()->has($ip)) {
|
||||
$searchArray = CIDRStorage::getInstance()->get($ip);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isInRange($ip, $results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = null;
|
||||
$result = shell_exec('whois ' . $ip . ' | grep CIDR');
|
||||
if ($result) {
|
||||
preg_match('/^CIDR:\s*(.*)$/m', $result, $matches);
|
||||
$search = $matches[1] ?? null;
|
||||
}
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep inetnum',
|
||||
'head -n 1',
|
||||
"awk '{print $2\"-\"$4}'",
|
||||
'sed "s/-$//"',
|
||||
'xargs ipcalc',
|
||||
"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep IPv4',
|
||||
'grep " - "',
|
||||
'head -n 1',
|
||||
"awk '{print $3\"-\"$5}'",
|
||||
'xargs ipcalc',
|
||||
"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search = strtr($search, ["\n" => ' ', ', ' => ' ']);
|
||||
$searchArray = array_filter(
|
||||
explode(' ', strtr($search, ' ', '')),
|
||||
fn(string $cidr) => strlen($cidr) > 0
|
||||
);
|
||||
CIDRStorage::getInstance()->set($ip, $searchArray);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
} else {
|
||||
App::getLogger()->error($ip . ' -> CIDR not found', [$i + 1 . '/' . $count]);
|
||||
}
|
||||
delay(0.001);
|
||||
})->await();
|
||||
}
|
||||
|
||||
return self::minimizeSubnets($results);
|
||||
}
|
||||
|
||||
public static function trimCIDRs(array $searchArray): array {
|
||||
$subnets = [];
|
||||
foreach ($searchArray as $search) {
|
||||
foreach (explode(' ', $search) as $cidr) {
|
||||
if (str_contains($cidr, '/')) {
|
||||
$subnets[] = trim($cidr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
public static function sortSubnets(array $subnets): array {
|
||||
usort($subnets, function ($a, $b) {
|
||||
return (int) explode('/', $a)[1] - (int) explode('/', $b)[1];
|
||||
});
|
||||
usort($subnets, function ($a, $b) {
|
||||
return ip2long(explode('/', $a)[0]) - ip2long(explode('/', $b)[0]);
|
||||
});
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
public static function minimizeSubnets(array $subnets): array {
|
||||
$result = [];
|
||||
foreach (self::sortSubnets(array_filter($subnets, fn(string $subnet) => !!$subnet)) as $subnet) {
|
||||
$include = true;
|
||||
[$ip /*, $mask*/] = explode('/', $subnet);
|
||||
$ipLong = ip2long($ip);
|
||||
// $maskLong = ~((1 << 32 - (int) $mask) - 1);
|
||||
|
||||
foreach ($result as $resSubnet) {
|
||||
[$resIp, $resMask] = explode('/', $resSubnet);
|
||||
$resIpLong = ip2long($resIp);
|
||||
$resMaskLong = ~((1 << 32 - (int) $resMask) - 1);
|
||||
|
||||
if (($ipLong & $resMaskLong) === ($resIpLong & $resMaskLong)) {
|
||||
$include = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($include) {
|
||||
$result[] = $subnet;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function isInRange(string $ip, array $cidrs): bool {
|
||||
foreach ($cidrs as $cidr) {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
if ((ip2long($ip) & ~((1 << 32 - $mask) - 1)) === ip2long($subnet)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
176
src/Domain/Helper/IP6Helper.php
Normal file
176
src/Domain/Helper/IP6Helper.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\Storage\CIDRStorage;
|
||||
|
||||
use function Amp\async;
|
||||
use function Amp\delay;
|
||||
|
||||
class IP6Helper {
|
||||
public static function processCIDR(array $ips, $results = []): array {
|
||||
$count = count($ips);
|
||||
foreach ($ips as $i => $ip) {
|
||||
if ($ip === '::1') {
|
||||
continue;
|
||||
}
|
||||
async(function () use ($ip, $i, $count, &$results) {
|
||||
if (CIDRStorage::getInstance()->has($ip)) {
|
||||
$searchArray = CIDRStorage::getInstance()->get($ip);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isInRange($ip, $results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep inet6num',
|
||||
'grep -v "/0"',
|
||||
'head -n 1',
|
||||
"awk '{print $2}'",
|
||||
])
|
||||
);
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep route6',
|
||||
'grep -v "/0"',
|
||||
'head -n 1',
|
||||
"awk '{print $2}'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search = strtr($search, ["\n" => ' ', ', ' => ' ']);
|
||||
$searchArray = array_filter(
|
||||
explode(' ', strtr($search, ' ', '')),
|
||||
fn(string $cidr) => strlen($cidr) > 0
|
||||
);
|
||||
CIDRStorage::getInstance()->set($ip, $searchArray);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
} else {
|
||||
App::getLogger()->error($ip . ' -> CIDR not found', [$i + 1 . '/' . $count]);
|
||||
}
|
||||
delay(0.001);
|
||||
})->await();
|
||||
}
|
||||
|
||||
//return self::minimizeSubnets($results);
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $searchArray
|
||||
* @return array
|
||||
*/
|
||||
public static function trimCIDRs(array $searchArray): array {
|
||||
$subnets = [];
|
||||
foreach ($searchArray as $search) {
|
||||
foreach (explode(' ', $search) as $cidr) {
|
||||
if (str_contains($cidr, '/')) {
|
||||
[$address, $prefix] = explode('/', trim($cidr));
|
||||
$subnets[] = $address . '/' . max($prefix, 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $subnets
|
||||
* @return array
|
||||
*/
|
||||
public static function sortSubnets(array $subnets): array {
|
||||
usort($subnets, function ($a, $b) {
|
||||
return (int) explode('/', $a)[1] - (int) explode('/', $b)[1];
|
||||
});
|
||||
|
||||
usort($subnets, function ($a, $b) {
|
||||
$ipA = inet_pton(explode('/', $a)[0]);
|
||||
$ipB = inet_pton(explode('/', $b)[0]);
|
||||
|
||||
return strcmp($ipA, $ipB);
|
||||
});
|
||||
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $subnets
|
||||
* @return array
|
||||
*/
|
||||
public static function minimizeSubnets(array $subnets): array {
|
||||
$result = [];
|
||||
foreach (self::sortSubnets(array_filter($subnets, fn(string $subnet) => !!$subnet)) as $subnet) {
|
||||
if (!$subnet) {
|
||||
continue;
|
||||
}
|
||||
[$address, $prefix] = explode('/', $subnet);
|
||||
|
||||
$addressNum = inet_pton($address);
|
||||
$addressNum = unpack('J', $addressNum)[1];
|
||||
|
||||
$isUnique = true;
|
||||
foreach ($result as $existingCidr) {
|
||||
[$existingAddress, $existingPrefix] = explode('/', $existingCidr);
|
||||
$existingAddressNum = inet_pton($existingAddress);
|
||||
$existingAddressNum = unpack('J', $existingAddressNum)[1];
|
||||
|
||||
$mask = (1 << 128) - (1 << 128 - $prefix);
|
||||
$existingMask = (1 << 128) - (1 << 128 - $existingPrefix);
|
||||
if (($addressNum & $mask) === ($existingAddressNum & $existingMask)) {
|
||||
$isUnique = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isUnique) {
|
||||
$result[] = $subnet;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function isInRange(string $ip, array $cidrs): bool {
|
||||
$ip = inet_pton($ip);
|
||||
|
||||
foreach ($cidrs as $cidr) {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
$subnet = inet_pton($subnet);
|
||||
|
||||
$mask = intval($mask);
|
||||
$binaryMask = str_repeat('f', $mask >> 2);
|
||||
switch ($mask % 4) {
|
||||
case 1:
|
||||
$binaryMask .= '8';
|
||||
break;
|
||||
case 2:
|
||||
$binaryMask .= 'c';
|
||||
break;
|
||||
case 3:
|
||||
$binaryMask .= 'e';
|
||||
break;
|
||||
}
|
||||
$binaryMask = str_pad($binaryMask, 32, '0');
|
||||
$mask = pack('H*', $binaryMask);
|
||||
|
||||
if (($ip & $mask) === ($subnet & $mask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
107
src/Infrastructure/API/App.php
Normal file
107
src/Infrastructure/API/App.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
8
src/Infrastructure/API/AppModuleInterface.php
Normal file
8
src/Infrastructure/API/AppModuleInterface.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API;
|
||||
|
||||
interface AppModuleInterface {
|
||||
public function start(): void;
|
||||
public function stop(): void;
|
||||
}
|
17
src/Infrastructure/API/Handler/HTTPErrorHandler.php
Normal file
17
src/Infrastructure/API/Handler/HTTPErrorHandler.php
Normal 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])
|
||||
);
|
||||
}
|
||||
}
|
58
src/Infrastructure/API/Handler/HTTPHandler.php
Normal file
58
src/Infrastructure/API/Handler/HTTPHandler.php
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
12
src/Infrastructure/API/Handler/HTTPHandlerInterface.php
Normal file
12
src/Infrastructure/API/Handler/HTTPHandlerInterface.php
Normal 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;
|
||||
}
|
27
src/Infrastructure/API/Handler/Handler.php
Normal file
27
src/Infrastructure/API/Handler/Handler.php
Normal 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']);
|
||||
}
|
||||
}
|
12
src/Infrastructure/API/Handler/HandlerInterface.php
Normal file
12
src/Infrastructure/API/Handler/HandlerInterface.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API\Handler;
|
||||
|
||||
use Amp\Http\Server\RequestHandler;
|
||||
|
||||
interface HandlerInterface {
|
||||
/**
|
||||
* @return RequestHandler
|
||||
*/
|
||||
public function getHandler(): RequestHandler;
|
||||
}
|
119
src/Infrastructure/API/Server.php
Normal file
119
src/Infrastructure/API/Server.php
Normal 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();
|
||||
}
|
||||
}
|
44
src/Infrastructure/Storage/CIDRStorage.php
Normal file
44
src/Infrastructure/Storage/CIDRStorage.php
Normal 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']);
|
||||
}
|
||||
}
|
9
src/Infrastructure/Storage/StorageInterface.php
Normal file
9
src/Infrastructure/Storage/StorageInterface.php
Normal 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;
|
||||
}
|
20
src/Infrastructure/Task/ShellTask.php
Normal file
20
src/Infrastructure/Task/ShellTask.php
Normal 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);
|
||||
}
|
||||
}
|
11
src/Infrastructure/Task/TaskInterface.php
Normal file
11
src/Infrastructure/Task/TaskInterface.php
Normal 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;
|
||||
}
|
33
src/functions.php
Normal file
33
src/functions.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace OpenCCK;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return ?string
|
||||
*/
|
||||
function getEnv(string $key): ?string {
|
||||
if (!isset($_ENV[$key])) {
|
||||
return null;
|
||||
}
|
||||
if (preg_match('/^"(.*)"$/i', $_ENV[$key], $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return $_ENV[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug function
|
||||
* @param mixed $mixed
|
||||
* @param bool $exit
|
||||
* @return void
|
||||
*/
|
||||
function dbg(mixed $mixed, bool $exit = true): void {
|
||||
if (php_sapi_name() == 'cli') {
|
||||
echo print_r($mixed, true);
|
||||
} else {
|
||||
echo '<pre>' . print_r($mixed, true) . '</pre>';
|
||||
}
|
||||
if ($exit) {
|
||||
exit();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user