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,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()
: '');
}
}

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

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

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

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

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

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

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

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