Compare commits

..

No commits in common. "master" and "1.0.8" have entirely different histories.

34 changed files with 944 additions and 4567 deletions

3
.gitignore vendored
View File

@ -14,6 +14,3 @@
/config.json.prod /config.json.prod
keys.txt keys.txt
/.phpunit.cache /.phpunit.cache
/var/log/*
/public/openapi/bindapi.json
/public/openapi/bootstrap.php

View File

@ -6,6 +6,9 @@
6. [DynDNS](#6-dyndns) 6. [DynDNS](#6-dyndns)
7. [Conclusion](#7-conclusion) 7. [Conclusion](#7-conclusion)
Don't use this code right now.
You can try 1.0.2, but it's not well tested.
NOTICE: This documentation is not current as of September 2022. NOTICE: This documentation is not current as of September 2022.
After I finished the refactoring I'll upgrade it. After I finished the refactoring I'll upgrade it.

6
TODO
View File

@ -1,7 +1,3 @@
API Endpoint cleanup check keytype of panel/bindApi
check keytype of panel
check keytype of 1bindApi
check:configkey => update config.json
more UNIT tests more UNIT tests

View File

@ -10,11 +10,11 @@ if (php_sapi_name() !== 'cli') {
// check php version (must be >= 8.1) // check php version (must be >= 8.1)
/** @noinspection PhpArgumentWithoutNamedIdentifierInspection */ /** @noinspection PhpArgumentWithoutNamedIdentifierInspection */
if (version_compare(PHP_VERSION, '8.2.0', '<')) { if (version_compare(PHP_VERSION, '8.1.0', '<')) {
echo 'This application requires PHP 8.2 or newer. You are running ' . PHP_VERSION . PHP_EOL; echo 'This application requires PHP 8.1 or newer. You are running ' . PHP_VERSION . PHP_EOL;
echo 'If you are using KeyHelp, use keyhelp-php82 ' . $argv[0] . ' instead.' . PHP_EOL; echo 'If you are using KeyHelp, use keyhelp-php81 ' . $argv[0] . ' instead.' . PHP_EOL;
exit; exit;
} }
/** @noinspection PhpArgumentWithoutNamedIdentifierInspection */ /** @noinspection PhpArgumentWithoutNamedIdentifierInspection */
require dirname(__DIR__, 1) . '/src/Utilities/Console.php'; require dirname(__DIR__, 1) . '/src/Util/Console.php';

View File

@ -1,148 +0,0 @@
{
"openapi": "3.0.0",
"info": {
"title": "bindAPI",
"version": "1.0.9"
},
"servers": [
{
"url": "{schema}://{hostname}/api",
"description": "The bindAPI URL.",
"variables": {
"schema": {
"enum": [
"http",
"https"
],
"default": "https"
},
"hostname": {
"enum": [
"ns1.24unix.net",
"ns2.24unix.net"
],
"default": "ns2.24unix.net"
}
}
}
],
"paths": {
"/ping": {
"get": {
"tags": [
"Server"
],
"description": "Checks for connectivity and valid APIkey",
"operationId": "ping",
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "API key is missing or invalid."
}
},
"security": [
{
"Authorization": []
}
]
}
},
"/version": {
"get": {
"tags": [
"Server"
],
"description": "Check the API version of the nameserver.",
"operationId": "version",
"responses": {
"200": {
"description": "x.y.z, aka major, minor, patch"
},
"401": {
"description": "API key is missing or invalid."
}
},
"security": [
{
"Authorization": []
}
]
}
},
"/domains": {
"get": {
"tags": [
"Domains"
],
"summary": "List all domains.",
"description": "Returns a list of all domains on this server.",
"operationId": "getAllDomains",
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "API key is missing or invalid."
},
"404": {
"description": "Domain not found."
}
},
"security": [
{
"Authorization": []
}
]
}
},
"/domains/{name}": {
"get": {
"tags": [
"Domains"
],
"summary": "Returns a single domain.",
"description": "Returns information of a single domain specified by its domain name.",
"operationId": "getSingleDomain",
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "API key is missing or invalid."
},
"404": {
"description": "Domain not found."
}
},
"security": []
}
}
},
"components": {
"securitySchemes": {
"Authorization": {
"type": "apiKey",
"description": "Api Authentication",
"name": "X-API-Key",
"in": "header"
}
}
},
"tags": [
{
"name": "Server"
}
]
}

View File

@ -1,8 +1,8 @@
{ {
"name": "tracer/bindapi", "name": "24unix/bindapi",
"description": "manage Bind9 client zones for KeyHelp", "description": "manage Bind9 DNS server via REST API",
"version": "1.1.2", "version": "1.0.7",
"build_number": "380", "build_number": "351",
"authors": [ "authors": [
{ {
"name": "Micha Espey", "name": "Micha Espey",
@ -23,7 +23,6 @@
"ext-posix": "*", "ext-posix": "*",
"ext-sodium": "*", "ext-sodium": "*",
"arubacao/tld-checker": "^1.2", "arubacao/tld-checker": "^1.2",
"bartlett/php-compatinfo": "^7.1",
"monolog/monolog": "^3.1", "monolog/monolog": "^3.1",
"netresearch/jsonmapper": "^4.4", "netresearch/jsonmapper": "^4.4",
"php-di/php-di": "^6.3", "php-di/php-di": "^6.3",
@ -31,7 +30,7 @@
"robmorgan/phinx": "^0.15", "robmorgan/phinx": "^0.15",
"symfony/property-access": "^6.1", "symfony/property-access": "^6.1",
"symfony/serializer": "^6.1", "symfony/serializer": "^6.1",
"zircote/swagger-php": "^4.8" "zircote/swagger-php": "^4.2"
}, },
"config": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,

3015
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,9 @@
{ {
"env": "prod",
"dbHost": "localhost", "dbHost": "localhost",
"dbPort": 3306, "dbPort": 3306,
"dbDatabase": "sampledb", "dbDatabase": "sampledb",
"dbUser": "sampleuser", "dbUser": "sampleuser",
"dbPassword": "secret", "dbPassword": "secret",
"encryptionKey": "1bad::babe", "encryptionKey": "changeme",
"debug": false "debug": false
} }

View File

@ -1,37 +0,0 @@
<?php
use Phinx\Db\Adapter\MysqlAdapter;
class AddSelfToNameservers extends Phinx\Migration\AbstractMigration
{
public function change()
{
// $this->table('domains', [
// 'id' => false,
// 'primary_key' => ['id'],
// 'engine' => 'InnoDB',
// 'encoding' => 'utf8mb4',
// 'collation' => 'utf8mb4_unicode_ci',
// 'comment' => '',
// 'row_format' => 'DYNAMIC',
// ])
// ->removeColumn('self')
// ->save();
$this->table('nameservers', [
'id' => false,
'primary_key' => ['id'],
'engine' => 'InnoDB',
'encoding' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'comment' => '',
'row_format' => 'DYNAMIC',
])
->addColumn('self', 'enum', [
'null' => false,
'limit' => 3,
'values' => ['yes', 'no'],
'after' => 'apikey_prefix',
])
->save();
}
}

View File

@ -1,41 +0,0 @@
<?php
use Phinx\Db\Adapter\MysqlAdapter;
class UUIDForConfig extends Phinx\Migration\AbstractMigration
{
public function change()
{
$this->table('config', [
'id' => false,
'primary_key' => ['id'],
'engine' => 'InnoDB',
'encoding' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'comment' => '',
'row_format' => 'DYNAMIC',
])
->addColumn('id', 'uuid', [
'null' => false,
])
->changeColumn('name', 'string', [
'null' => false,
'limit' => 256,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
'after' => 'id',
])
->changeColumn('value', 'string', [
'null' => false,
'limit' => 256,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
'after' => 'name',
])
->addIndex(['id'], [
'name' => 'id',
'unique' => true,
])
->save();
}
}

View File

@ -1,24 +0,0 @@
<?php
use Phinx\Db\Adapter\MysqlAdapter;
class DefaultUUIDforConfig extends Phinx\Migration\AbstractMigration
{
public function change()
{
$this->table('config', [
'id' => false,
'primary_key' => ['id'],
'engine' => 'InnoDB',
'encoding' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'comment' => '',
'row_format' => 'DYNAMIC',
])
->changeColumn('id', 'uuid', [
'null' => false,
'default' => 'uuid()',
])
->save();
}
}

View File

@ -112,36 +112,12 @@ return array (
), ),
'columns' => 'columns' =>
array ( array (
'id' =>
array (
'TABLE_CATALOG' => 'def',
'TABLE_NAME' => 'config',
'COLUMN_NAME' => 'id',
'ORDINAL_POSITION' => 1,
'COLUMN_DEFAULT' => 'uuid()',
'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'uuid',
'CHARACTER_MAXIMUM_LENGTH' => NULL,
'CHARACTER_OCTET_LENGTH' => NULL,
'NUMERIC_PRECISION' => NULL,
'NUMERIC_SCALE' => NULL,
'DATETIME_PRECISION' => NULL,
'CHARACTER_SET_NAME' => NULL,
'COLLATION_NAME' => NULL,
'COLUMN_TYPE' => 'uuid',
'COLUMN_KEY' => 'PRI',
'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references',
'COLUMN_COMMENT' => '',
'IS_GENERATED' => 'NEVER',
'GENERATION_EXPRESSION' => NULL,
),
'name' => 'name' =>
array ( array (
'TABLE_CATALOG' => 'def', 'TABLE_CATALOG' => 'def',
'TABLE_NAME' => 'config', 'TABLE_NAME' => 'config',
'COLUMN_NAME' => 'name', 'COLUMN_NAME' => 'name',
'ORDINAL_POSITION' => 2, 'ORDINAL_POSITION' => 1,
'COLUMN_DEFAULT' => NULL, 'COLUMN_DEFAULT' => NULL,
'IS_NULLABLE' => 'NO', 'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'varchar', 'DATA_TYPE' => 'varchar',
@ -153,7 +129,7 @@ return array (
'CHARACTER_SET_NAME' => 'utf8mb4', 'CHARACTER_SET_NAME' => 'utf8mb4',
'COLLATION_NAME' => 'utf8mb4_general_ci', 'COLLATION_NAME' => 'utf8mb4_general_ci',
'COLUMN_TYPE' => 'varchar(256)', 'COLUMN_TYPE' => 'varchar(256)',
'COLUMN_KEY' => 'UNI', 'COLUMN_KEY' => 'PRI',
'EXTRA' => '', 'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references', 'PRIVILEGES' => 'select,insert,update,references',
'COLUMN_COMMENT' => '', 'COLUMN_COMMENT' => '',
@ -165,7 +141,7 @@ return array (
'TABLE_CATALOG' => 'def', 'TABLE_CATALOG' => 'def',
'TABLE_NAME' => 'config', 'TABLE_NAME' => 'config',
'COLUMN_NAME' => 'value', 'COLUMN_NAME' => 'value',
'ORDINAL_POSITION' => 3, 'ORDINAL_POSITION' => 2,
'COLUMN_DEFAULT' => NULL, 'COLUMN_DEFAULT' => NULL,
'IS_NULLABLE' => 'NO', 'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'varchar', 'DATA_TYPE' => 'varchar',
@ -187,24 +163,6 @@ return array (
), ),
'indexes' => 'indexes' =>
array ( array (
'PRIMARY' =>
array (
1 =>
array (
'Table' => 'config',
'Non_unique' => 0,
'Key_name' => 'PRIMARY',
'Seq_in_index' => 1,
'Column_name' => 'id',
'Collation' => 'A',
'Sub_part' => NULL,
'Packed' => NULL,
'Null' => '',
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
),
),
'name' => 'name' =>
array ( array (
1 => 1 =>
@ -223,24 +181,6 @@ return array (
'Index_comment' => '', 'Index_comment' => '',
), ),
), ),
'id' =>
array (
1 =>
array (
'Table' => 'config',
'Non_unique' => 0,
'Key_name' => 'id',
'Seq_in_index' => 1,
'Column_name' => 'id',
'Collation' => 'A',
'Sub_part' => NULL,
'Packed' => NULL,
'Null' => '',
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
),
),
), ),
'foreign_keys' => NULL, 'foreign_keys' => NULL,
), ),
@ -1186,30 +1126,6 @@ return array (
'IS_GENERATED' => 'NEVER', 'IS_GENERATED' => 'NEVER',
'GENERATION_EXPRESSION' => NULL, 'GENERATION_EXPRESSION' => NULL,
), ),
'self' =>
array (
'TABLE_CATALOG' => 'def',
'TABLE_NAME' => 'nameservers',
'COLUMN_NAME' => 'self',
'ORDINAL_POSITION' => 7,
'COLUMN_DEFAULT' => NULL,
'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'enum',
'CHARACTER_MAXIMUM_LENGTH' => 3,
'CHARACTER_OCTET_LENGTH' => 12,
'NUMERIC_PRECISION' => NULL,
'NUMERIC_SCALE' => NULL,
'DATETIME_PRECISION' => NULL,
'CHARACTER_SET_NAME' => 'utf8mb4',
'COLLATION_NAME' => 'utf8mb4_unicode_ci',
'COLUMN_TYPE' => 'enum(\'yes\',\'no\')',
'COLUMN_KEY' => '',
'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references',
'COLUMN_COMMENT' => '',
'IS_GENERATED' => 'NEVER',
'GENERATION_EXPRESSION' => NULL,
),
), ),
'indexes' => 'indexes' =>
array ( array (

View File

@ -1,4 +1,4 @@
Copy these files to /etc/systems/system, adapt the path in the service unit and enable the timer by issuing: Copy this files to /etc/systems/system, adapt the path in the service unit and enable the timer by issuing:
systemctl daemon-reload systemctl daemon-reload
systemctl enable bindAPI.timer systemctl enable bindAPI.timer

View File

@ -2,5 +2,4 @@
Description=BindAPI Service to check zone file and reload configuration Description=BindAPI Service to check zone file and reload configuration
[Service] [Service]
User=<paneluser> ExecStart=/home/users/<user>/<bindApi>/bin/console cron:run -q
ExecStart=/home/users/<user>/<bindApi>/bin/console -q cron:run

View File

@ -6,15 +6,10 @@ error_reporting(error_level: E_ALL);
require dirname(path: __DIR__) . '/vendor/autoload.php'; require dirname(path: __DIR__) . '/vendor/autoload.php';
$parsedUrl = parse_url(url: $_SERVER['REQUEST_URI'], component: PHP_URL_PATH); $uri = parse_url(url: $_SERVER['REQUEST_URI'], component: PHP_URL_PATH);
$uri = explode(separator: '/', string: $parsedUrl); $uri = explode(separator: '/', string: $uri);
$baseRoutes = ['app', 'api']; if ($uri[1] !== 'api') {
$uriPrefix = $uriFirstThreeLetters = substr(string: $uri[1], offset: 0, length: 3);
if (!in_array(needle: $uriPrefix, haystack: $baseRoutes)) {
// only handle $baseRoutes, elso go to swagger ui
$scheme = $_SERVER['REQUEST_SCHEME']; $scheme = $_SERVER['REQUEST_SCHEME'];
$host = $_SERVER['SERVER_NAME']; $host = $_SERVER['SERVER_NAME'];
$header = "$scheme://$host/openapi/index.html"; $header = "$scheme://$host/openapi/index.html";
@ -26,16 +21,10 @@ header(header: "Access-Control-Allow-Origin: *");
header(header: "Content-Type: application/json; charset=UTF-8"); header(header: "Content-Type: application/json; charset=UTF-8");
header(header: "Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE"); header(header: "Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header(header: "Access-Control-Max-Age: 3600"); header(header: "Access-Control-Max-Age: 3600");
header(header: "Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With, x-api-key"); header(header: "Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
$requestMethod = $_SERVER["REQUEST_METHOD"]; $requestMethod = $_SERVER["REQUEST_METHOD"];
if ($requestMethod === "OPTIONS") {
// Respond with OK status code for preflight requests
http_response_code(response_code: 200);
exit();
}
try { try {
$app = new BindAPI(quiet: false); $app = new BindAPI(quiet: false);
$app->handleRequest(requestMethod: $requestMethod, uri: $uri); $app->handleRequest(requestMethod: $requestMethod, uri: $uri);
@ -44,4 +33,3 @@ try {
'error' => $e->getMessage() 'error' => $e->getMessage()
]); ]);
} }

View File

@ -1,5 +0,0 @@
<?php
const DEFAULT_NS = 'ns2.24unix.net';
const NAMESERVERS = ['ns1.24unix.net', 'ns2.24unix.net'];

View File

@ -19,8 +19,7 @@
<script> <script>
window.onload = function () { window.onload = function () {
// Begin Swagger UI call region // Begin Swagger UI call region
let ui; const ui = SwaggerUIBundle({
ui = SwaggerUIBundle({
url: "/openapi/bindapi.json", url: "/openapi/bindapi.json",
dom_id: "#swagger-ui", dom_id: "#swagger-ui",
deepLinking: true, deepLinking: true,

View File

@ -1,13 +1,25 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace App\Service; namespace App\Controller;
use UnhandledMatchError; use UnhandledMatchError;
error_reporting(error_level: E_ALL); error_reporting(error_level: E_ALL);
class ApiClient class ApiController
{ {
/**
* @param String $requestType
* @param String $serverName
* @param int $versionIP
* @param String $apiKey
* @param String $command
* @param String $serverType
* @param array $body
*
* @return array
*/
function sendCommand(string $requestType, string $serverName, int $versionIP, string $apiKey, string $command, string $serverType, array $body = []): array function sendCommand(string $requestType, string $serverName, int $versionIP, string $apiKey, string $command, string $serverType, array $body = []): array
{ {
$error = false; $error = false;
@ -63,7 +75,6 @@ class ApiClient
break; break;
case 400: case 400:
$result = $resultJSON; $result = $resultJSON;
$error = true;
break; break;
case 401: case 401:
$result = 'Missing or wrong API Key'; $result = 'Missing or wrong API Key';
@ -71,14 +82,11 @@ class ApiClient
break; break;
case 404: case 404:
$result = '404 Not Found'; $result = '404 Not Found';
$error = true;
break; break;
case 500: case 500:
$result = 'server error'; $result = 'server error';
$error = true;
break; break;
default: default:
$error = true;
$result = 'Unhandled error: ' . $httpResponse; $result = 'Unhandled error: ' . $httpResponse;
} }
} else { } else {
@ -105,7 +113,7 @@ class ApiClient
$options = array( $options = array(
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '', CURLOPT_ENCODING => "",
CURLOPT_AUTOREFERER => true, CURLOPT_AUTOREFERER => true,
CURLOPT_CONNECTTIMEOUT => 120, CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120, CURLOPT_TIMEOUT => 120,
@ -131,6 +139,4 @@ class ApiClient
return $header; return $header;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,6 @@
namespace App\Controller\Commands; namespace App\Controller\Commands;
use App\Utilities\Colors;
/** /**
* *
*/ */
@ -33,17 +31,17 @@ class CommandGroup
public function printCommands(int $longestCommandLength): void public function printCommands(int $longestCommandLength): void
{ {
echo Colors::YELLOW . str_pad(string: $this->name, length: $longestCommandLength + 1) . Colors::WHITE . $this->description . Colors::DEFAULT . PHP_EOL; echo COLOR_YELLOW . str_pad(string: $this->name, length: $longestCommandLength + 1) . COLOR_WHITE . $this->description . COLOR_DEFAULT . PHP_EOL;
foreach ($this->commands as $command) { foreach ($this->commands as $command) {
echo Colors::GREEN . str_pad(string: ' ', length: $longestCommandLength + 1, pad_type: STR_PAD_LEFT) . $this->name . ':' . $command->getName(); echo COLOR_GREEN . str_pad(string: ' ', length: $longestCommandLength + 1, pad_type: STR_PAD_LEFT) . $this->name . ':' . $command->getName();
foreach ($command->getMandatoryParameters() as $optionals) { foreach ($command->getMandatoryParameters() as $optionals) {
echo ' <' . $optionals . '>'; echo ' <' . $optionals . '>';
} }
foreach ($command->getOptionalParameters() as $mandatory) { foreach ($command->getOptionalParameters() as $mandatory) {
echo ' {' . $mandatory . '}'; echo ' {' . $mandatory . '}';
} }
echo Colors::WHITE . ' ' . $command->getDescription(); echo COLOR_WHITE . ' ' . $command->getDescription();
echo Colors::DEFAULT . PHP_EOL; echo COLOR_DEFAULT . PHP_EOL;
} }
} }

View File

@ -2,8 +2,6 @@
namespace App\Controller\Commands; namespace App\Controller\Commands;
use App\Utilities\Colors;
/** /**
* *
*/ */
@ -66,23 +64,23 @@ class CommandGroupContainer
if ($group->exec(subcommand: $subcommand)) { if ($group->exec(subcommand: $subcommand)) {
exit(0); exit(0);
} else { } else {
echo Colors::DEFAULT . 'Unknown subcommand ' . Colors::YELLOW . $subcommand . Colors::DEFAULT .' for ' . Colors::YELLOW . $command . Colors::DEFAULT . '.' . PHP_EOL; echo COLOR_DEFAULT . 'Unknown subcommand ' . COLOR_YELLOW . $subcommand . COLOR_DEFAULT .' for ' . COLOR_YELLOW . $command . COLOR_DEFAULT . '.' . PHP_EOL;
exit(1); exit(1);
} }
} else { } else {
echo Colors::DEFAULT . 'Unknown command group ' . Colors::YELLOW . $command . Colors::DEFAULT . '.' . PHP_EOL; echo COLOR_DEFAULT . 'Unknown command group ' . COLOR_YELLOW . $command . COLOR_DEFAULT . '.' . PHP_EOL;
exit(1); exit(1);
} }
} else { } else {
// check for command group and print available commands // check for command group and print available commands
foreach ($this->commandGroups as $group) { foreach ($this->commandGroups as $group) {
if ($group->getName() === $command) { if ($group->getName() === $command) {
echo 'Available subcommands for: ' . Colors::YELLOW . $group->getName() . Colors::DEFAULT . ':' . PHP_EOL; echo 'Available subcommands for: ' . COLOR_YELLOW . $group->getName() . COLOR_DEFAULT . ':' . PHP_EOL;
$group->printCommands(strlen(string: $group->getName())); $group->printCommands(strlen(string: $group->getName()));
exit(0); exit(0);
} }
} }
} }
echo Colors::DEFAULT . 'Unknown command ' . Colors::YELLOW . $command . Colors::DEFAULT . '.' . PHP_EOL; echo COLOR_DEFAULT . 'Unknown command ' . COLOR_YELLOW . $command . COLOR_DEFAULT . '.' . PHP_EOL;
} }
} }

View File

@ -2,14 +2,11 @@
namespace App\Controller; namespace App\Controller;
use App\Utilities\Colors;
class ConfigController class ConfigController
{ {
private array $config; private array $config;
private static $missingEncryptionShown = false;
public function __construct(private readonly bool $quiet, bool $test = false) public function __construct(bool $quiet, bool $test = false)
{ {
if ($test) { if ($test) {
@ -38,33 +35,21 @@ class ConfigController
} }
$configJSON = file_get_contents(filename: $configFile); $configJSON = file_get_contents(filename: $configFile);
// first check if json is valid, after make the assignment if (json_decode(json: $configJSON) === null) {
if (json_decode(json: $configJSON, associative: true) === null) {
echo 'Config file is not valid JSON.' . PHP_EOL; echo 'Config file is not valid JSON.' . PHP_EOL;
echo $configJSON . PHP_EOL; echo $configJSON . PHP_EOL;
exit(1); exit(1);
} }
$this->config = json_decode(json: $configJSON, associative: true); $this->config = json_decode(json: $configJSON, associative: true);
if (!ConfigController::$missingEncryptionShown) {
if (!isset($this->config['encryptionKey']) || ($this->config['encryptionKey'] === '1bad::babe')) { $this->config['quiet'] = (bool)$quiet;
ConfigController::$missingEncryptionShown = true; $this->config['test'] = (bool)$test;
if (!$this->quiet) {
echo Colors::RED . 'Error: ' . Colors::DEFAULT . 'No encryption key, please run ' . Colors::YELLOW . './bin/console check:generatekey' . Colors::DEFAULT . PHP_EOL;
}
exit(1);
}
} }
} public function getConfig(string $configKey): string
public function getConfig(string $configKey): ?string
{ {
if (isset($this->config[$configKey])) {
return $this->config[$configKey]; return $this->config[$configKey];
} else {
return null;
}
} }
} }

View File

@ -6,8 +6,6 @@ use App\Entity\Domain;
use App\Repository\DomainRepository; use App\Repository\DomainRepository;
use App\Repository\NameserverRepository; use App\Repository\NameserverRepository;
use App\Repository\PanelRepository; use App\Repository\PanelRepository;
use App\Service\ApiClient;
use App\Utilities\Colors;
use Monolog\Logger; use Monolog\Logger;
error_reporting(error_level: E_ALL); error_reporting(error_level: E_ALL);
@ -27,7 +25,7 @@ class DomainController
public function __construct( public function __construct(
private readonly NameserverRepository $nameserverRepository, private readonly NameserverRepository $nameserverRepository,
private readonly ApiClient $checkController, private readonly ApiController $checkController,
private readonly DomainRepository $domainRepository, private readonly DomainRepository $domainRepository,
private readonly PanelRepository $panelRepository, private readonly PanelRepository $panelRepository,
private readonly ConfigController $configController, private readonly ConfigController $configController,
@ -83,13 +81,13 @@ class DomainController
foreach ($domains as $domain) { foreach ($domains as $domain) {
$zoneFile = $this->localZonesDir . $domain->getName(); $zoneFile = $this->localZonesDir . $domain->getName();
if (!$this->quiet) { if (!$this->quiet) {
echo ' ' . Colors::YELLOW . str_pad(string: $domain->getName(), length: $longestEntry + 1, pad_string: " ", pad_type: STR_PAD_RIGHT) ; echo ' ' . COLOR_YELLOW . str_pad(string: $domain->getName(), length: $longestEntry + 1, pad_string: " ", pad_type: STR_PAD_RIGHT) ;
} }
if (strcmp(string1: $self->getName(), string2: $domain->getPanel()) !== 0) { if (strcmp(string1: $self->getName(), string2: $domain->getPanel()) !== 0) {
if (!file_exists(filename: $zoneFile)) { if (!file_exists(filename: $zoneFile)) {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::GREEN . ' OK' . Colors::DEFAULT . PHP_EOL; echo COLOR_GREEN . ' OK' . COLOR_DEFAULT . PHP_EOL;
} }
$this->createSlaveZoneFile(domain: $domain); $this->createSlaveZoneFile(domain: $domain);
} else { } else {
@ -101,19 +99,17 @@ class DomainController
echo 'missing value: ' . $zoneFile; echo 'missing value: ' . $zoneFile;
} }
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . 'Zone already exists.' . PHP_EOL; echo COLOR_DEFAULT . 'Zone already exists.' . PHP_EOL;
} }
} }
} else { } else {
if (!$this->quiet) { echo COLOR_DEFAULT . 'We are master for ' . COLOR_YELLOW . $domain->getName() . PHP_EOL;
echo Colors::DEFAULT . 'We are master for ' . Colors::YELLOW . $domain->getName() . PHP_EOL;
}
} }
} }
// remove stale zones // remove stale zones
foreach ($existingZones as $zone) { foreach ($existingZones as $zone) {
if (!$this->quiet) { if (!$this->quiet) {
echo 'Removing stale zone: ' . Colors::YELLOW . $zone . Colors::DEFAULT . PHP_EOL; echo 'Removing stale zone: ' . COLOR_YELLOW . $zone . COLOR_DEFAULT . PHP_EOL;
} }
echo $zone . PHP_EOL; echo $zone . PHP_EOL;
unlink(filename: $zone); unlink(filename: $zone);
@ -191,12 +187,12 @@ class DomainController
$uid = posix_geteuid(); $uid = posix_geteuid();
} }
if (!$this->quiet) { if (!$this->quiet) {
echo "UID:\t" . Colors::YELLOW . $uid . PHP_EOL; echo "UID:\t" . COLOR_YELLOW . $uid . PHP_EOL;
} }
$pwuid = posix_getpwuid(user_id: $uid); $pwuid = posix_getpwuid(user_id: $uid);
$name = $pwuid['name']; $name = $pwuid['name'];
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . "Name:\t" . Colors::YELLOW . $name . PHP_EOL; echo COLOR_DEFAULT . "Name:\t" . COLOR_YELLOW . $name . PHP_EOL;
} }
if (!$bindGroup = posix_getgrnam(name: 'bind')) { if (!$bindGroup = posix_getgrnam(name: 'bind')) {
@ -205,40 +201,40 @@ class DomainController
$members = $bindGroup['members'] ?? []; $members = $bindGroup['members'] ?? [];
if (in_array(needle: $name, haystack: $members)) { if (in_array(needle: $name, haystack: $members)) {
if (!$this->quiet) { if (!$this->quiet) {
echo "\t$name" . Colors::DEFAULT . ' is in group ' . Colors::YELLOW . 'bind' . PHP_EOL; echo "\t$name" . COLOR_DEFAULT . ' is in group ' . COLOR_YELLOW . 'bind' . PHP_EOL;
} }
} else { } else {
$setupIsValid = false; $setupIsValid = false;
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::RED . "\t$name needs to be in group " . Colors::YELLOW . 'bind' . Colors::DEFAULT . '!' . PHP_EOL; echo COLOR_RED . "\t$name needs to be in group " . COLOR_YELLOW . 'bind' . COLOR_DEFAULT . '!' . PHP_EOL;
} }
} }
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . 'Checking ' . Colors::YELLOW . $this->localZoneFile . PHP_EOL; echo COLOR_DEFAULT . 'Checking ' . COLOR_YELLOW . $this->localZoneFile . PHP_EOL;
} }
$localZoneFilePermissions = @fileperms(filename: $this->localZoneFile); $localZoneFilePermissions = @fileperms(filename: $this->localZoneFile);
if ($localZoneFilePermissions & 0x0010) { if ($localZoneFilePermissions & 0x0010) {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . "\t✅ Group has write access." . PHP_EOL; echo COLOR_DEFAULT . "\t✅ Group has write access." . PHP_EOL;
} }
} else { } else {
$setupIsValid = false; $setupIsValid = false;
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::RED . "\t❌Group needs write permission!" . Colors::DEFAULT . PHP_EOL; echo COLOR_RED . "\t❌Group needs write permission!" . COLOR_DEFAULT . PHP_EOL;
} }
} }
if (!$this->quiet) { if (!$this->quiet) {
echo 'Checking ' . Colors::YELLOW . $this->namedConfLocalFile . PHP_EOL; echo 'Checking ' . COLOR_YELLOW . $this->namedConfLocalFile . PHP_EOL;
} }
if (file_exists(filename: $this->namedConfLocalFile) && $namedConfLocal = file_get_contents(filename: $this->namedConfLocalFile)) { if (file_exists(filename: $this->namedConfLocalFile) && $namedConfLocal = file_get_contents(filename: $this->namedConfLocalFile)) {
if (!str_contains(haystack: $namedConfLocal, needle: $this->localZoneFile)) { if (!str_contains(haystack: $namedConfLocal, needle: $this->localZoneFile)) {
$setupIsValid = false; $setupIsValid = false;
if (!$this->quiet) { if (!$this->quiet) {
echo "\t$this->localZoneFile" . Colors::RED . ' needs to be included in ' . Colors::YELLOW . $this->namedConfLocalFile . PHP_EOL; echo "\t$this->localZoneFile" . COLOR_RED . ' needs to be included in ' . COLOR_YELLOW . $this->namedConfLocalFile . PHP_EOL;
} }
} else { } else {
if (!$this->quiet) { if (!$this->quiet) {
echo "\t$this->localZoneFile" . Colors::DEFAULT . ' is included in ' . Colors::YELLOW . $this->namedConfLocalFile . PHP_EOL; echo "\t$this->localZoneFile" . COLOR_DEFAULT . ' is included in ' . COLOR_YELLOW . $this->namedConfLocalFile . PHP_EOL;
} }
} }
} else { } else {
@ -248,7 +244,7 @@ class DomainController
} }
} }
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . 'Checking directory: ' . Colors::YELLOW . $this->localZonesDir . PHP_EOL; echo COLOR_DEFAULT . 'Checking directory: ' . COLOR_YELLOW . $this->localZonesDir . PHP_EOL;
} }
$localZoneDirPermissions = @fileperms(filename: $this->localZonesDir); $localZoneDirPermissions = @fileperms(filename: $this->localZonesDir);
if ($localZoneDirPermissions & 0x0010) { if ($localZoneDirPermissions & 0x0010) {
@ -258,7 +254,7 @@ class DomainController
} else { } else {
$setupIsValid = false; $setupIsValid = false;
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::RED . "\t❌Group needs write permission!" . PHP_EOL; echo COLOR_RED . "\t❌Group needs write permission!" . PHP_EOL;
} }
} }
return $setupIsValid; return $setupIsValid;
@ -272,7 +268,7 @@ class DomainController
{ {
if (!file_exists(filename: $this->localZoneFile)) { if (!file_exists(filename: $this->localZoneFile)) {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . 'Local Zone file ' . Colors::YELLOW . $this->localZoneFile . Colors::DEFAULT . ' does not exist.' . PHP_EOL; echo COLOR_DEFAULT . 'Local Zone file ' . COLOR_YELLOW . $this->localZoneFile . COLOR_DEFAULT . ' does not exist.' . PHP_EOL;
} }
exit(1); exit(1);
} }
@ -283,33 +279,33 @@ class DomainController
foreach ($domains as $domain) { foreach ($domains as $domain) {
$idString = '(' . $domain->getId() . ') '; $idString = '(' . $domain->getId() . ') ';
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::YELLOW . echo COLOR_YELLOW .
str_pad(string: $domain->getName(), length: $maxNameLength + 1) str_pad(string: $domain->getName(), length: $maxNameLength + 1)
. Colors::DEFAULT . COLOR_DEFAULT
. str_pad(string: $idString, length: 7, pad_type: STR_PAD_LEFT); . str_pad(string: $idString, length: 7, pad_type: STR_PAD_LEFT);
} }
$hasError = false; $hasError = false;
if ($this->isMasterZone(domain: $domain)) { if ($this->isMasterZone(domain: $domain)) {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::GREEN . 'Master Zone'; echo COLOR_GREEN . 'Master Zone';
} }
} else { } else {
if (!str_contains(haystack: $localZones, needle: $domain->getName())) { if (!str_contains(haystack: $localZones, needle: $domain->getName())) {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::RED . 'is missing in ' . Colors::YELLOW . $this->localZoneFile . Colors::DEFAULT; echo COLOR_RED . 'is missing in ' . COLOR_YELLOW . $this->localZoneFile . COLOR_DEFAULT;
} }
$hasError = true; $hasError = true;
} else { } else {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::GREEN . 'OK'; echo COLOR_GREEN . 'OK';
} }
} }
$zoneFile = $this->localZonesDir . $domain->getName(); $zoneFile = $this->localZonesDir . $domain->getName();
if (!file_exists(filename: $zoneFile)) { if (!file_exists(filename: $zoneFile)) {
echo ' Missing zone file for ' . Colors::YELLOW . $zoneFile . Colors::DEFAULT; echo ' Missing zone file for ' . COLOR_YELLOW . $zoneFile . COLOR_DEFAULT;
$hasError = true; $hasError = true;
} }
@ -318,7 +314,7 @@ class DomainController
} }
} }
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::DEFAULT . PHP_EOL; echo COLOR_DEFAULT . PHP_EOL;
} }
} }
@ -368,7 +364,7 @@ class DomainController
return true; return true;
} else { } else {
if (!$this->quiet) { if (!$this->quiet) {
echo Colors::RED . ' Error: ' . Colors::DEFAULT . 'unable to create ' . Colors::YELLOW . $this->localZonesDir . $domainName . Colors::DEFAULT . PHP_EOL; echo COLOR_RED . ' Error: ' . COLOR_DEFAULT . 'unable to create ' . COLOR_YELLOW . $this->localZonesDir . $domainName . COLOR_DEFAULT . PHP_EOL;
} }
return false; return false;
} }

View File

@ -58,7 +58,7 @@ class EncryptionController
$plain = sodium_crypto_secretbox_open(ciphertext: $ciphertext, nonce: $nonce, key: $binKey); $plain = sodium_crypto_secretbox_open(ciphertext: $ciphertext, nonce: $nonce, key: $binKey);
if ($plain === false) { if ($plain === false) {
throw new Exception(message: ' Incorrect key.' . PHP_EOL); throw new Exception(message: ' Incorrect key.');
} }
sodium_memzero(string: $ciphertext); sodium_memzero(string: $ciphertext);
sodium_memzero(string: $key); sodium_memzero(string: $key);

View File

@ -10,40 +10,39 @@ use App\Repository\ApikeyRepository;
use App\Repository\DomainRepository; use App\Repository\DomainRepository;
use App\Repository\DynDNSRepository; use App\Repository\DynDNSRepository;
use App\Repository\PanelRepository; use App\Repository\PanelRepository;
use App\Service\ApiClient;
use Monolog\Logger; use Monolog\Logger;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OAT;
use OpenApi\Attributes\OpenApi;
use OpenApi\Generator;
use UnhandledMatchError; use UnhandledMatchError;
use function Symfony\Component\String\s;
// TODO attributes for swaggerUI // TODO attributes for swaggerUI
#[OA\Info(version: VERSION, title: 'bindAPI')] /**
#[OA\Server( *
*/
#[OAT\Info(version: '0.0.1', title: 'bindAPI')]
#[OAT\Server(
url: "{schema}://{hostname}/api", url: "{schema}://{hostname}/api",
description: "The bindAPI URL.", description: "The bindAPI URL.",
variables: [ variables: [
new OA\ServerVariable( new OAT\ServerVariable(
serverVariable: 'schema', serverVariable: "schema",
default: 'https', default: "https",
enum: ['http', 'https'] enum: ["https", "http"]
), ),
new OA\ServerVariable( new OAT\ServerVariable(
serverVariable: 'hostname', serverVariable: "hostname",
default: DEFAULT_NS, default: "ns2.24unix.net",
enum: NAMESERVERS
) )
] ]
)] )]
#[OA\Tag( #[OAT\Tag(
name: "Server" name: "Server"
)] )]
#[OA\SecurityScheme( #[OAT\SecurityScheme(
securityScheme: "Authorization", securityScheme: "Authorization",
type: "apiKey", type: "apiKey",
description: "Api Authentication", description: "description",
name: "X-API-Key", name: "X-API-Key",
in: "header" in: "header"
)] )]
@ -57,96 +56,60 @@ class RequestController
private array $uri; private array $uri;
// server tag /**
private string $baseDir; * @param ApiController $apiController
* @param ApikeyRepository $apikeyRepository
#[OA\Get( * @param DomainController $domainController
path: '/ping', * @param DomainRepository $domainRepository
operationId: 'ping', * @param DynDNSRepository $dynDNSRepository
description: 'Checks for connectivity and valid APIkey', * @param PanelRepository $panelRepository
security: [ * @param ConfigController $configController
['Authorization' => []] * @param EncryptionController $encryptionController
], * @param Logger $logger
tags: ['Server'], */
responses: [ public function __construct(
new OA\Response( private readonly ApiController $apiController,
response: 200, private readonly ApikeyRepository $apikeyRepository,
description: 'OK' private readonly DomainController $domainController,
), private readonly DomainRepository $domainRepository,
new OA\Response( private readonly DynDNSRepository $dynDNSRepository,
response: 401, private readonly PanelRepository $panelRepository,
description: 'API key is missing or invalid.' private readonly ConfigController $configController,
) private readonly EncryptionController $encryptionController,
] private readonly Logger $logger)
)]
private function handlePing(): void
{ {
if ($this->validateApiKey()) { $this->status = '';
$this->status = '200 OK'; $this->response = '';
$this->response = 'pong'; $this->message = '';
} else { $this->result = [];
$this->status = '401 Unauthorized';
$this->message = 'API key is missing or invalid';
}
} }
#[OA\Get( /**
path: '/version', * @return void
operationId: 'version', */
description: 'Check the API version of the nameserver.', #[OAT\Get(
security: [
['Authorization' => []]
],
tags: ['Server'],
responses: [
new OA\Response(
response: 200,
description: 'x.y.z, aka major, minor, patch'
),
new OA\Response(
response: 401,
description: 'API key is missing or invalid.'
)
]
)]
private function getVersion(): void
{
if ($this->validateApiKey()) {
$this->status = '200 OK';
$composerJson = json_decode(json: file_get_contents(filename: $this->baseDir . 'composer.json'));
$version = $composerJson->version;
$buildNumber = $composerJson->build_number;
$this->result = [
'version' => $version,
'buildnumber' => $buildNumber,
];
} else {
$this->status = '401 Unauthorized';
$this->message = 'API key is missing or invalid';
}
}
#[OA\Get(
path: '/domains', path: '/domains',
operationId: 'getAllDomains', operationId: 'getAllDomains',
description: 'Returns a list of all domains on this server.', description: 'Returns a list of all domains on this server.',
summary: 'List all domains.', summary: 'Listing all domains.',
security: [ // security: [
['Authorization' => []] // 'Authorization' => [
], //
// "read:api"
// ]
// ],
servers: [],
tags: ['Domains'], tags: ['Domains'],
responses: [ responses: [
new OA\Response( new OAT\Response(
response: 200, response: 200,
description: 'OK' description: 'OK'
), ),
new OA\Response( new OAT\Response(
response: 401, response: 401,
description: 'API key is missing or invalid.' description: 'API key is missing or invalid.'
), ),
new OA\Response( new OAT\Response(
response: 404, response: 404,
description: 'Domain not found.' description: 'Domain not found.'
)] )]
@ -168,6 +131,16 @@ class RequestController
/** /**
*/ */
private function handlePing(): void
{
if ($this->checkPassword()) {
$this->status = '200 OK';
$this->response = 'pong';
} else {
$this->status = '401 Unauthorized';
$this->message = 'API key is missing or invalid';
}
}
/** /**
@ -175,7 +148,7 @@ class RequestController
*/ */
private function handleDomains(): void private function handleDomains(): void
{ {
if ($this->validateApiKey()) { if ($this->checkPassword()) {
try { try {
match ($this->requestMethod) { match ($this->requestMethod) {
'GET' => $this->handleDomainsGetRequest(), 'GET' => $this->handleDomainsGetRequest(),
@ -191,8 +164,131 @@ class RequestController
} }
/**
* @OA\Tag(name = "Server")
* @OA\Get(
* path = "/ping",
* summary = "Returning pong.",
* description = "Can be used to check API or server availability.",
* tags={"Server"},
* @OA\Response(response = "200", description = "OK"),
* @OA\Response(response = "401", description = "API key is missing or invalid."),
* security={
* {"Authorization":{"read"}}
* }
* )
*
* @OA\Tag(name = "Domains")
* @OA\Put(
* path="/domains/{name}",
* summary="Updates a domain.",
* description="Updates a domain. Only supplied fields will be updated, existing won't be affected.",
* tags={"Domains"},
* @OA\Response(response="200", description="OK"),
* @OA\Response(response = "401", description = "API key is missing or invalid."),
* @OA\Response(response="404", description="Domain not found."),
* security={
* {"Authorization":{"read":"write"}}
* }
* )
* @OA\Delete (
* path="/domains/{name}",
* summary="Deletes a domain.",
* description="Deletes a domain.",
* tags={"Domains"},
* @OA\Response(response="200", description="OK"),
* @OA\Response(response = "401", description = "API key is missing or invalid."),
* @OA\Response(response="404", description="Domain not found."),
* security={
* {"Authorization":{"read":"write"}}
* }
* )
* @param string $requestMethod
* @param array $uri
*
* @return void
*/
private function validateApiKey(): bool #[
OAT\Get(
path: '/domains/{name}',
operationId: 'getSingleDomain',
description: 'Returns information of a single domain specified by its domain name.',
summary: 'Returns a single domain.',
security: [
],
tags: ['Domains'],
parameters: [
new OAT\Parameter(name: 'name', in: 'path', required: true, schema: new OAT\Schema(type: 'string')),
],
responses: [
new OAT\Response(
response: 200,
description: 'OK'
),
new OAT\Response(
response: 401,
description: 'API key is missing or invalid.'
),
new OAT\Response(
response: 404,
description: 'Domain not found.'
)]
)]
public function handleRequest(string $requestMethod, array $uri): void
{
$this->logger->debug(message: "Request: $requestMethod $uri[1]");
$this->requestMethod = strtoupper(string: $requestMethod);
$this->uri = $uri;
$command = $this->uri[2];
if (empty($command) || !(($command == 'domains') || ($command == 'ping') || ($command == 'apidoc') || ($command == 'dyndns'))) {
$this->status = "404 Not Found";
$this->message = "Endpoint not found.";
} else {
try {
match ($command) {
'dyndns' => $this->handleDynDNS(),
'ping' => $this->handlePing(),
'domains' => $this->handleDomains(),
};
} catch (UnhandledMatchError) {
$this->status = '400 Bad Request';
$this->message = 'Unknown path: ' . $command;
}
}
if (!empty($this->status)) {
header(header: $_SERVER['SERVER_PROTOCOL'] . ' ' . $this->status);
}
if (!empty($this->response)) {
echo json_encode(value: [
'response' => $this->response
]);
} elseif (!empty($this->result)) {
echo json_encode(value: [
'result' => $this->result
]);
} elseif (!empty($this->message)) {
echo json_encode(value: [
'message' => $this->message
]);
} else {
echo json_encode(value: [
'message' => $this->message ?? 'Error: No message.'
]);
}
}
/**
* @return bool
*/
private function checkPassword(): bool
{ {
$headers = array_change_key_case(array: getallheaders(), case: CASE_UPPER); $headers = array_change_key_case(array: getallheaders(), case: CASE_UPPER);
$apiKey = $headers['X-API-KEY'] ?? ''; $apiKey = $headers['X-API-KEY'] ?? '';
@ -222,33 +318,10 @@ class RequestController
return true; return true;
} }
#[OA\Get(
path: '/domains/{name}',
operationId: 'getSingleDomain',
description: 'Returns information of a single domain specified by its domain name.',
summary: 'Returns a single domain.',
security: [
['Authorization' => []]
],
tags: ['Domains'],
parameters: [
new OA\Parameter(name: 'name', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'OK'
),
new OA\Response(
response: 401,
description: 'API key is missing or invalid.'
),
new OA\Response(
response: 404,
description: 'Domain not found.'
)]
)] /**
* @return void
*/
private function handleDomainsGetRequest(): void private function handleDomainsGetRequest(): void
{ {
$name = $this->uri[3] ?? ''; $name = $this->uri[3] ?? '';
@ -360,6 +433,9 @@ class RequestController
} }
/**
* @return void
*/
private function handleDomainsDeleteRequest(): void private function handleDomainsDeleteRequest(): void
{ {
$deleteData = fopen(filename: 'php://input', mode: 'r'); $deleteData = fopen(filename: 'php://input', mode: 'r');
@ -394,7 +470,7 @@ class RequestController
{ {
$this->logger->debug(message: 'handleDynDNS()'); $this->logger->debug(message: 'handleDynDNS()');
if ($this->validateApiKey()) { if ($this->checkPassword()) {
$host = $this->uri[3] ?? ''; $host = $this->uri[3] ?? '';
if (empty($host)) { if (empty($host)) {
@ -461,7 +537,7 @@ class RequestController
$panel = $this->panelRepository->findByName(name: $domain->getPanel()); $panel = $this->panelRepository->findByName(name: $domain->getPanel());
if (!empty($panel->getAaaa())) { if (!empty($panel->getAaaa())) {
$domainData = $this->apiClient->sendCommand( $domainData = $this->apiController->sendCommand(
requestType: 'GET', requestType: 'GET',
serverName: $panel->getName(), serverName: $panel->getName(),
versionIP: 6, versionIP: 6,
@ -469,7 +545,7 @@ class RequestController
command: 'domains/name/' . $domainName, command: 'domains/name/' . $domainName,
serverType: 'panel'); serverType: 'panel');
} else { } else {
$domainData = $this->apiClient->sendCommand( $domainData = $this->apiController->sendCommand(
requestType: 'GET', requestType: 'GET',
serverName: $panel->getName(), serverName: $panel->getName(),
versionIP: 4, versionIP: 4,
@ -482,7 +558,7 @@ class RequestController
$domainID = $domainDecodedData->id; $domainID = $domainDecodedData->id;
if (!empty($panel->getAaaa())) { if (!empty($panel->getAaaa())) {
$dnsData = $this->apiClient->sendCommand( $dnsData = $this->apiController->sendCommand(
requestType: 'GET', requestType: 'GET',
serverName: $panel->getName(), serverName: $panel->getName(),
versionIP: 6, versionIP: 6,
@ -490,7 +566,7 @@ class RequestController
command: 'dns/' . $domainID, command: 'dns/' . $domainID,
serverType: 'panel'); serverType: 'panel');
} else { } else {
$dnsData = $this->apiClient->sendCommand( $dnsData = $this->apiController->sendCommand(
requestType: 'GET', requestType: 'GET',
serverName: $panel->getName(), serverName: $panel->getName(),
versionIP: 4, versionIP: 4,
@ -531,7 +607,7 @@ class RequestController
]); ]);
if (!empty($panel->getAaaa())) { if (!empty($panel->getAaaa())) {
$result = $this->apiClient->sendCommand( $result = $this->apiController->sendCommand(
requestType: 'PUT', requestType: 'PUT',
serverName: $panel->getName(), serverName: $panel->getName(),
versionIP: 6, versionIP: 6,
@ -541,7 +617,7 @@ class RequestController
body: json_decode(json: $newDnsData, associative: true) body: json_decode(json: $newDnsData, associative: true)
); );
} else { } else {
$result = $this->apiClient->sendCommand( $result = $this->apiController->sendCommand(
requestType: 'PUT', requestType: 'PUT',
serverName: $panel->getName(), serverName: $panel->getName(),
versionIP: 4, versionIP: 4,
@ -569,6 +645,11 @@ class RequestController
} }
/**
* @param String $host
*
* @return string
*/
private function getDomain(string $host): string private function getDomain(string $host): string
{ {
$host = strtolower(string: trim(string: $host)); $host = strtolower(string: trim(string: $host));
@ -583,89 +664,5 @@ class RequestController
return $host; return $host;
} }
// private function apiDoc(): void
// {
// $srcDir = dirname(path: __DIR__);
// $requestControllerPath = $srcDir . '/Controller/RequestController.php';
//
// $openApi = Generator::scan(sources: [$requestControllerPath]);
// header(header: 'Content-Type: application/json');
//
// echo $openApi->toJson();
// exit(0);
// }
public function __construct(
private readonly ApiClient $apiClient,
private readonly ApikeyRepository $apikeyRepository,
private readonly DomainController $domainController,
private readonly DomainRepository $domainRepository,
private readonly DynDNSRepository $dynDNSRepository,
private readonly PanelRepository $panelRepository,
private readonly ConfigController $configController,
private readonly EncryptionController $encryptionController,
private readonly Logger $logger)
{
$this->baseDir = dirname(path: __DIR__, levels: 2) . '/';
$this->status = '';
$this->response = '';
$this->message = '';
$this->result = [];
}
public function handleRequest(string $requestMethod, array $uri): void
{
$this->logger->debug(message: "Request: $requestMethod $uri[1]");
$this->requestMethod = strtoupper(string: $requestMethod);
$this->uri = $uri;
$command = $this->uri[2];
// use my router class from address book?
$routes = ['ping', 'version', 'domains', 'apidoc', 'dyndns'];
if (empty($command) || !(in_array(needle: $command, haystack: $routes))) {
$this->status = "404 Not Found";
$this->message = "Endpoint not found.";
} else {
try {
match ($command) {
// server
'ping' => $this->handlePing(),
'version' => $this->getVersion(),
// domains
'domains' => $this->handleDomains(),
'dyndns' => $this->handleDynDNS(),
// 'apidoc' => $this->apiDoc(),
};
} catch (UnhandledMatchError) {
$this->status = '400 Bad Request';
$this->message = 'Unknown path: ' . $command;
}
}
// process api requests
if (!empty($this->status)) {
header(header: $_SERVER['SERVER_PROTOCOL'] . ' ' . $this->status);
}
if (!empty($this->response)) {
echo json_encode(value: [
'response' => $this->response
]);
} elseif (!empty($this->result)) {
echo json_encode(value: $this->result);
} elseif (!empty($this->message)) {
echo json_encode(value: [
'message' => $this->message
]);
} else {
echo json_encode(value: [
'message' => $this->message ?? 'Error: No message.'
]);
}
}
} }

View File

@ -15,19 +15,6 @@ class Security
private bool $hstsInclude; private bool $hstsInclude;
private bool $hstsPreload; private bool $hstsPreload;
private bool $isPreferHttps;
public function isPreferHttps(): bool
{
return $this->isPreferHttps;
}
public function setIsPreferHttps(bool $isPreferHttps): void
{
$this->isPreferHttps = $isPreferHttps;
}
/** /**
* @return int * @return int
*/ */

View File

@ -12,6 +12,7 @@ use SodiumException;
* *
*/ */
#[OAT\Schema(schema: 'nameserver')] #[OAT\Schema(schema: 'nameserver')]
class Nameserver class Nameserver
{ {
/** /**
@ -30,9 +31,7 @@ class Nameserver
private string $aaaa = '', private string $aaaa = '',
private readonly string $passphrase = '', private readonly string $passphrase = '',
private string $apikey = '', private string $apikey = '',
private string $apikeyPrefix = '', private string $apikeyPrefix = '')
private string $self = 'no'
)
{ {
if ($this->passphrase) { if ($this->passphrase) {
$configController = new ConfigController(quiet: false); $configController = new ConfigController(quiet: false);
@ -51,16 +50,6 @@ class Nameserver
} }
public function getSelf(): string
{
return $this->self;
}
public function setSelf(string $self): void
{
$this->self = $self;
}
/** /**
* @return string * @return string
*/ */

View File

@ -5,7 +5,6 @@ namespace App\Provider;
//error_reporting(error_level: E_ALL); //error_reporting(error_level: E_ALL);
use App\Utilities\Colors;
use PDO; use PDO;
use PDOException; use PDOException;
use PHPUnit\Exception; use PHPUnit\Exception;
@ -28,29 +27,11 @@ class DatabaseConnection
public function __construct(private readonly ConfigController $configController) public function __construct(private readonly ConfigController $configController)
{ {
$errors = []; $dbHost = $this->configController->getConfig(configKey: 'dbHost');
if (!$dbHost = $this->configController->getConfig(configKey: 'dbHost')) { $dbPort = $this->configController->getConfig(configKey: 'dbPort');
$errors[] = Colors::RED . 'Error: ' . Colors::DEFAULT . 'Missing config: dbHost' . PHP_EOL; $dbDatabase = $this->configController->getConfig(configKey: 'dbDatabase');
} $dbUser = $this->configController->getConfig(configKey: 'dbUser');
if (!$dbPort = $this->configController->getConfig(configKey: 'dbPort')) { $dbPassword = $this->configController->getConfig(configKey: 'dbPassword');
$errors[] = Colors::RED . 'Error: ' . Colors::DEFAULT . 'Missing config: dbPort}' . PHP_EOL;
}
if (!$dbDatabase = $this->configController->getConfig(configKey: 'dbDatabase')) {
$errors[] = Colors::RED . 'Error: ' . Colors::DEFAULT . 'Missing config: dbDatabase' . PHP_EOL;
}
if (!$dbUser = $this->configController->getConfig(configKey: 'dbUser')) {
$errors[] = Colors::RED . 'Error: ' . Colors::DEFAULT . 'Missing config: dbUser' . PHP_EOL;
}
if (!$dbPassword = $this->configController->getConfig(configKey: 'dbPassword')) {
$errors[] = Colors::RED . 'Error: ' . Colors::DEFAULT . 'Missing config: dbPassword' . PHP_EOL;
}
if ($errors) {
foreach ($errors as $error) {
echo $error;
}
exit(1);
}
try { try {
$this->dbConnection = new PDO( $this->dbConnection = new PDO(
@ -73,8 +54,8 @@ class DatabaseConnection
$result = $statement->fetch(); $result = $statement->fetch();
if (empty($result)) { if (empty($result)) {
// ALTER TABLE `domains` ADD `panel_id` INT NULL AFTER `id`; // ALTER TABLE `domains` ADD `panel_id` INT NULL AFTER `id`;
echo Colors::RED . 'Error: ' . Colors::DEFAULT . 'Cannot find tables.' . PHP_EOL; echo COLOR_RED . 'Error: ' . COLOR_DEFAULT . 'Cannot find tables.' . PHP_EOL;
echo 'Run the migration: ' . Colors::YELLOW . './bin/console migrations:migrate' . Colors::DEFAULT . PHP_EOL; echo 'Run the migration: ' . COLOR_YELLOW . './bin/console migrations:migrate' . COLOR_DEFAULT . PHP_EOL;
} }
} catch (PDOException $exception) { } catch (PDOException $exception) {
echo $exception->getMessage() . PHP_EOL; echo $exception->getMessage() . PHP_EOL;
@ -90,7 +71,6 @@ class DatabaseConnection
} }
} }
} }
function generatePassword(int $length = 8): string function generatePassword(int $length = 8): string
{ {
$chars = '23456789bcdfhkmnprstvzBCDFHJKLMNPRSTVZ'; $chars = '23456789bcdfhkmnprstvzBCDFHJKLMNPRSTVZ';

View File

@ -26,7 +26,7 @@ class NameserverRepository
{ {
$nameservers = []; $nameservers = [];
$sql = " $sql = "
SELECT id, name, a, aaaa, apikey, apikey_prefix, self SELECT id, name, a, aaaa, apikey, apikey_prefix
FROM " . DatabaseConnection::TABLE_NAMESERVERS . " FROM " . DatabaseConnection::TABLE_NAMESERVERS . "
ORDER BY name"; ORDER BY name";
@ -34,7 +34,7 @@ class NameserverRepository
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql); $statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->execute(); $statement->execute();
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) { while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
$nameserver = new Nameserver(name: $result['name'], id: $result['id'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix'], self: $result['self']); $nameserver = new Nameserver(name: $result['name'], id: $result['id'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix']);
$nameservers[] = $nameserver; $nameservers[] = $nameserver;
} }
return $nameservers; return $nameservers;
@ -50,7 +50,7 @@ class NameserverRepository
public function findFirst(): ?Nameserver public function findFirst(): ?Nameserver
{ {
$sql = " $sql = "
SELECT id, name, a, aaaa, apikey, apikey_prefix, self SELECT id, name, a, aaaa, apikey, apikey_prefix
FROM " . DatabaseConnection::TABLE_NAMESERVERS . " FROM " . DatabaseConnection::TABLE_NAMESERVERS . "
ORDER BY name"; ORDER BY name";
@ -58,7 +58,7 @@ class NameserverRepository
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql); $statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->execute(); $statement->execute();
$result = $statement->fetch(mode: PDO::FETCH_ASSOC); $result = $statement->fetch(mode: PDO::FETCH_ASSOC);
return new Nameserver(name: $result['name'], id: $result['id'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix'], self: $result['self']); return new Nameserver(name: $result['name'], id: $result['id'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix']);
} catch (PDOException $e) { } catch (PDOException $e) {
exit($e->getMessage()); exit($e->getMessage());
} }
@ -73,7 +73,7 @@ class NameserverRepository
public function findByID(int $id): ?Nameserver public function findByID(int $id): ?Nameserver
{ {
$sql = " $sql = "
SELECT id, name, a, aaaa, apikey, apikey_prefix, self SELECT id, name, a, aaaa, apikey, apikey_prefix
FROM . " . DatabaseConnection::TABLE_NAMESERVERS . " FROM . " . DatabaseConnection::TABLE_NAMESERVERS . "
WHERE id = :id"; WHERE id = :id";
@ -82,7 +82,7 @@ class NameserverRepository
$statement->bindParam(param: ':id', var: $id); $statement->bindParam(param: ':id', var: $id);
$statement->execute(); $statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) { if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new Nameserver(name: $result['name'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix'], self: $result['self']); return new Nameserver(name: $result['name'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix']);
} else { } else {
return null; return null;
} }
@ -100,7 +100,7 @@ class NameserverRepository
public function findByName(string $name): ?Nameserver public function findByName(string $name): ?Nameserver
{ {
$sql = " $sql = "
SELECT id, name, a, aaaa, apikey, apikey_prefix, self SELECT id, name, a, aaaa, apikey, apikey_prefix
FROM " . DatabaseConnection::TABLE_NAMESERVERS . " FROM " . DatabaseConnection::TABLE_NAMESERVERS . "
WHERE name = :name"; WHERE name = :name";
@ -109,7 +109,7 @@ class NameserverRepository
$statement->bindParam(param: ':name', var: $name); $statement->bindParam(param: ':name', var: $name);
$statement->execute(); $statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) { if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new Nameserver(name: $result['name'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix'], self: $result['self']); return new Nameserver(name: $result['name'], a: $result['a'], aaaa: $result['aaaa'], apikey: $result['apikey'], apikeyPrefix: $result['apikey_prefix']);
} else { } else {
return null; return null;
} }
@ -131,17 +131,11 @@ class NameserverRepository
$aaaa = $nameserver->getAaaa(); $aaaa = $nameserver->getAaaa();
$apikey = $nameserver->getApikey(); $apikey = $nameserver->getApikey();
$apikeyPrefix = $nameserver->getApikeyPrefix(); $apikeyPrefix = $nameserver->getApikeyPrefix();
$self = $nameserver->getSelf();
if ($self === '') {
$selfValue = 'no';
} else {
$selfValue = $self;
}
$sql = " $sql = "
INSERT INTO " . DatabaseConnection::TABLE_NAMESERVERS . " (name, a, aaaa, apikey, apikey_prefix, self) INSERT INTO " . DatabaseConnection::TABLE_NAMESERVERS . " (name, a, aaaa, apikey, apikey_prefix)
VALUES (:name, :a, :aaaa, :apikey, :apikey_prefix, :self)"; VALUES (:name, :a, :aaaa, :apikey, :apikey_prefix)";
try { try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql); $statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
@ -150,7 +144,6 @@ class NameserverRepository
$statement->bindParam(param: ':aaaa', var: $aaaa); $statement->bindParam(param: ':aaaa', var: $aaaa);
$statement->bindParam(param: ':apikey', var: $apikey); $statement->bindParam(param: ':apikey', var: $apikey);
$statement->bindParam(param: ':apikey_prefix', var: $apikeyPrefix); $statement->bindParam(param: ':apikey_prefix', var: $apikeyPrefix);
$statement->bindParam(param: ':self', var: $selfValue);
$statement->execute(); $statement->execute();
return intval(value: $this->databaseConnection->getConnection()->lastInsertId()); return intval(value: $this->databaseConnection->getConnection()->lastInsertId());
@ -173,7 +166,6 @@ class NameserverRepository
$apikey = $nameserver->getApikey(); $apikey = $nameserver->getApikey();
$apikeyPrefix = $nameserver->getApikeyPrefix(); $apikeyPrefix = $nameserver->getApikeyPrefix();
$passphrase = $nameserver->getPassphrase(); $passphrase = $nameserver->getPassphrase();
$self =$nameserver->getSelf();
$current = $this->findByID(id: $id); $current = $this->findByID(id: $id);
@ -193,10 +185,6 @@ class NameserverRepository
$apikeyPrefix = $current->getApikeyPrefix(); $apikeyPrefix = $current->getApikeyPrefix();
} }
if (empty($self)) {
$self = $current->getSelf();
}
$sql = " $sql = "
UPDATE " . DatabaseConnection::TABLE_NAMESERVERS . " SET UPDATE " . DatabaseConnection::TABLE_NAMESERVERS . " SET
@ -204,8 +192,7 @@ class NameserverRepository
a = :a, a = :a,
aaaa = :aaaa, aaaa = :aaaa,
apikey = :apikey, apikey = :apikey,
apikey_prefix = :apikey_prefix, apikey_prefix = :apikey_prefix
self = :self
WHERE id = :id"; WHERE id = :id";
try { try {
@ -216,7 +203,6 @@ class NameserverRepository
$statement->bindParam(param: 'aaaa', var: $aaaa); $statement->bindParam(param: 'aaaa', var: $aaaa);
$statement->bindParam(param: 'apikey', var: $apikey); $statement->bindParam(param: 'apikey', var: $apikey);
$statement->bindParam(param: 'apikey_prefix', var: $apikeyPrefix); $statement->bindParam(param: 'apikey_prefix', var: $apikeyPrefix);
$statement->bindParam(param: 'self', var: $self);
$statement->execute(); $statement->execute();
try { try {
sodium_memzero(string: $apikey); sodium_memzero(string: $apikey);

View File

@ -40,19 +40,13 @@ readonly class SettingsRepository
public function set(string $name, string $value): int public function set(string $name, string $value): int
{ {
$currentSetting = $this->findByName($name);
if ($currentSetting !== false) {
$sql = " $sql = "
UPDATE " . DatabaseConnection::TABLE_SETTINGS . " INSERT INTO " . DatabaseConnection::TABLE_SETTINGS . " (name, value)
SET value = :value VALUES (:name, :value)
WHERE name = :name ON DUPLICATE KEY UPDATE
value = :value
"; ";
} else {
$sql = "
INSERT INTO " . DatabaseConnection::TABLE_SETTINGS . " (id, name, value)
VALUES (UUID(), :name, :value)
";
}
try { try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql); $statement = $this->databaseConnection->getConnection()->prepare(query: $sql);

View File

@ -10,7 +10,6 @@ use App\Controller\DomainController;
use App\Controller\RequestController; use App\Controller\RequestController;
use App\Repository\DomainRepository; use App\Repository\DomainRepository;
use App\Repository\DynDNSRepository; use App\Repository\DynDNSRepository;
use App\Service\ApiClient;
use DI\Container; use DI\Container;
use DI\ContainerBuilder; use DI\ContainerBuilder;
use DI\DependencyException; use DI\DependencyException;
@ -27,6 +26,9 @@ class BindAPI
private Logger $logger; private Logger $logger;
private Container $container; private Container $container;
/**
* @throws Exception
*/
public function __construct(bool $quiet) public function __construct(bool $quiet)
{ {
// init the logger // init the logger
@ -36,9 +38,9 @@ class BindAPI
$debug = (new ConfigController(quiet: $quiet))->getConfig(configKey: 'debug'); $debug = (new ConfigController(quiet: $quiet))->getConfig(configKey: 'debug');
if ($debug) { if ($debug) {
$stream = new StreamHandler(stream: dirname(path: __DIR__, levels: 2) . '/var/log/bindAPI.debug', level: Level::Debug); $stream = new StreamHandler(stream: dirname(path: __DIR__, levels: 2) . '/bindAPI.log', level: Level::Debug);
} else { } else {
$stream = new StreamHandler(stream: dirname(path: __DIR__, levels: 2) . '/var/log/bindAPI.info', level: Level::Info); $stream = new StreamHandler(stream: dirname(path: __DIR__, levels: 2) . '/bindAPI.log', level: Level::Info);
} }
$stream->setFormatter(formatter: $formatter); $stream->setFormatter(formatter: $formatter);
@ -49,7 +51,6 @@ class BindAPI
$containerBuilder = new ContainerBuilder(); $containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([ $containerBuilder->addDefinitions([
ApiClient::class => autowire(),
ConfigController::class => autowire() ConfigController::class => autowire()
->constructorParameter(parameter: 'quiet', value: $quiet), ->constructorParameter(parameter: 'quiet', value: $quiet),
CLIController::class => autowire() CLIController::class => autowire()

View File

@ -1,13 +1,11 @@
<?php <?php
use App\Service\BindAPI; use App\Service\BindAPI;
use App\Utilities\Colors;
error_reporting(error_level: E_ALL & ~E_DEPRECATED); error_reporting(error_level: E_ALL & ~E_DEPRECATED);
if (!is_file(filename: dirname(path: __DIR__, levels: 2) . '/vendor/autoload.php')) { if (!is_file(filename: dirname(path: __DIR__, levels: 2) . '/vendor/autoload.php')) {
echo 'Required runtime components are missing. Try running "' . Colors::YELLOW . 'composer install' . Colors::DEFAULT . '".' . PHP_EOL; exit('Required runtime components are missing. Try running "composer install".' . PHP_EOL);
exit(1);
} }
require dirname(path: __DIR__, levels: 2) . '/vendor/autoload.php'; require dirname(path: __DIR__, levels: 2) . '/vendor/autoload.php';
@ -40,11 +38,11 @@ if (array_key_exists(key: 'v', array: $options) || array_key_exists(key: 'versio
$authorName = $authors[0]->name; $authorName = $authors[0]->name;
$authorEmail = $authors[0]->email; $authorEmail = $authors[0]->email;
echo "Name: $name" . PHP_EOL; echo 'Name: $name' . PHP_EOL;
echo "Description: $description" . PHP_EOL; echo 'Description: $description' . PHP_EOL;
echo "Version: $version" . PHP_EOL; echo 'Version: $version' . PHP_EOL;
echo "Build Number: $buildNumber" . PHP_EOL; echo 'Build Number: $buildNumber' . PHP_EOL;
echo "Author: $authorName ($authorEmail)" . PHP_EOL; echo 'Author: $authorName ($authorEmail)' . PHP_EOL;
exit(0); exit(0);
} }

View File

@ -1,13 +0,0 @@
<?php
namespace App\Utilities;
class Colors
{
const RED = "\033[31m";
const GREEN = "\033[32m";
const YELLOW = "\033[33m";
const BLUE = "\033[34m";
const WHITE = "\033[37m";
const DEFAULT = "\033[39m";
}