commit 13ee78438524a9714fb1006b99c7585bd585526f Author: tracer Date: Tue Jan 18 19:14:24 2022 +0100 initial commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/bindAPI.iml b/.idea/bindAPI.iml new file mode 100644 index 0000000..3d18e91 --- /dev/null +++ b/.idea/bindAPI.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..352a089 --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..773fb3d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,102 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c9bd698 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..a68c66b --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bindAPI/.gitignore b/bindAPI/.gitignore new file mode 100644 index 0000000..d08ff8b --- /dev/null +++ b/bindAPI/.gitignore @@ -0,0 +1,3 @@ +.idea + +/vendor/ diff --git a/bindAPI/bin/console b/bindAPI/bin/console new file mode 100755 index 0000000..e14b3a6 --- /dev/null +++ b/bindAPI/bin/console @@ -0,0 +1,111 @@ +#!/usr/bin/keyhelp-php81 + {A=} {AAAA=}" . PHP_EOL; + echo "\033[32m\tdomains:update {name} {A=} {AAAA=}" . PHP_EOL; + echo "\033[32m\tdomains:delete" . PHP_EOL; + echo "\033[32m\tdomains:check" . PHP_EOL; + + echo PHP_EOL . "\033[39me.g. ./bin/console apikey:list" . PHP_EOL; + + exit(0); +} + +//print(__DIR__ . PHP_EOL); +//print(dirname(__DIR__) . PHP_EOL); + +//exit; +require dirname(__DIR__) . '/vendor/autoload.php'; + +$dbConnection = (new DatabaseConnection())->getConnection(); +$apiUsers = new ApiUsers($dbConnection); +$domainController = new DomainController($dbConnection); + +[$command, $subcommand] = explode(':', $argv[1]); + +//echo $command, $subcommand; + +switch($command) { + case 'apikeys': + switch($subcommand) { + case "create"; + $result = $apiUsers->create(); + echo 'API key ' . $result['row'] . ' has been generated. Store it in a save place, it cannot be recovered.' . PHP_EOL; + echo "\033[32m\t" . $result['tokenPrefix'] . '.' . $result['key'] . PHP_EOL; + exit(0); + case "list": + echo 'All available API keys:' . PHP_EOL; + echo " No\tAPI key prefix" . PHP_EOL; + $keys = $apiUsers->findAll(); + if ($keys) { + foreach ($keys as $key) { + echo $key['id'] . "\t". $key['api_token_prefix'] . PHP_EOL; + } + } else { + echo 'No keys found.' . PHP_EOL; + } + exit(0); + case "delete": + $id = $argv[2] ?? 0; + if ($id == 0) { + echo 'You need to add the ID of the token.' .PHP_EOL; + exit(1); + } + if ($apiUsers->findByID($id)) { + $apiUsers->delete($id); + echo 'Token ' . $id . ' has been deleted.' . PHP_EOL; + exit(0); + } else { + echo 'Unknown ID: ' . $id . PHP_EOL; + exit(1); + } + default: + echo 'Unknown command: ' . $subcommand . PHP_EOL; + } + break; + case 'domains': + switch($subcommand) { + case 'list': + echo 'All available domains:' . PHP_EOL; + $domains = $domainController->findAll(); + if ($domains) { + $table = new \LucidFrame\Console\ConsoleTable(); + $table->setHeaders(['ID', 'Name', 'A', 'AAAA']); + foreach ($domains as $domain) { + $table->addRow([$domain['id'], $domain['name'], $domain['a'], $domain['aaaa']]); + } + $table->setPadding(2); + $table->display(); + } else { + echo 'No domains found.' . PHP_EOL; + } + exit(0); + + break; + default: + echo("Unknown Command: $subcommand" . PHP_EOL); + exit(1); + } + break; + default: + echo 'Unknown command: ' . $command . PHP_EOL; +} \ No newline at end of file diff --git a/bindAPI/composer.json b/bindAPI/composer.json new file mode 100644 index 0000000..51e1d08 --- /dev/null +++ b/bindAPI/composer.json @@ -0,0 +1,29 @@ +{ + "type": "project", + "license": "proprietary", + "minimum-stability": "stable", + "prefer-stable": true, + "require": { + "php": ">=8.1", + "ext-json": "*", + "ext-pdo": "*", + "phplucidframe/console-table": "^1.2" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Src\\": "src/" + } + } +} diff --git a/bindAPI/composer.lock b/bindAPI/composer.lock new file mode 100644 index 0000000..9f9b513 --- /dev/null +++ b/bindAPI/composer.lock @@ -0,0 +1,67 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0f698e244946cb8cd564781a86b98a1c", + "packages": [ + { + "name": "phplucidframe/console-table", + "version": "v1.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplucidframe/console-table.git", + "reference": "a973d911af96f3db42fca92cbeb1f473c9ad505e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplucidframe/console-table/zipball/a973d911af96f3db42fca92cbeb1f473c9ad505e", + "reference": "a973d911af96f3db42fca92cbeb1f473c9ad505e", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "LucidFrame\\": "src/", + "LucidFrameTest\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sithu K.", + "email": "cithukyaw@gmail.com" + } + ], + "description": "Console Table", + "support": { + "issues": "https://github.com/phplucidframe/console-table/issues", + "source": "https://github.com/phplucidframe/console-table/tree/v1.2.4" + }, + "time": "2019-03-03T12:17:32+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.1", + "ext-json": "*", + "ext-pdo": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/bindAPI/public/.htaccess b/bindAPI/public/.htaccess new file mode 100644 index 0000000..42fa810 --- /dev/null +++ b/bindAPI/public/.htaccess @@ -0,0 +1,11 @@ + + RewriteEngine On + RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$ + RewriteRule .* - [E=BASE:%1] + RewriteCond %{HTTP:Authorization} .+ + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + RewriteCond %{ENV:REDIRECT_STATUS} ="" + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ %{ENV:BASE}/index.php [L] + diff --git a/bindAPI/public/index.php b/bindAPI/public/index.php new file mode 100644 index 0000000..5661e43 --- /dev/null +++ b/bindAPI/public/index.php @@ -0,0 +1,39 @@ +getConnection(); + + +// TODO make a log class +$oFile = fopen ('log.txt', 'a'); + +$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +fputs($oFile, $uri); +$uri = explode( '/', $uri ); + +if ($uri[1] !== 'api') { + header("HTTP/1.1 404 Not Found"); + exit(); +} + +// TODO check for valid API key + +$requestMethod = $_SERVER["REQUEST_METHOD"]; + +// pass the request method and user ID to the PersonController and process the HTTP request: +$controller = new RequestController($dbConnection, $requestMethod, $uri); +$controller->processRequest(); + + +fclose($oFile); \ No newline at end of file diff --git a/bindAPI/src/Controller/.gitignore b/bindAPI/src/Controller/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/bindAPI/src/Controller/ApiUsers.php b/bindAPI/src/Controller/ApiUsers.php new file mode 100644 index 0000000..929d3ef --- /dev/null +++ b/bindAPI/src/Controller/ApiUsers.php @@ -0,0 +1,135 @@ +dbConnection = $dbConnection; + } + + /** + * @return array|false + */ + public function findAll(): bool|array + { + $statement = " + SELECT id, api_token_prefix, api_token + FROM user"; + + try { + $statement = $this->dbConnection->query($statement); + return $statement->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + exit($e->getMessage()); + } + } + + + /** + * @param Int $id + * + * @return array|false + */ + public function findByID(Int $id): bool|array + { + $statement = " + SELECT api_token_prefix, api_token + FROM user + WHERE id = :id; + "; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam(':id', $id); + $statement->execute(); + return $statement->fetchAll(PDO::FETCH_ASSOC); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } + + public function findByPrefix(String $prefix): bool|array + { + $statement = " + SELECT api_token + FROM user + WHERE api_token_prefix = :prefix; + "; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam(':prefix', $prefix); + $statement->execute(); + return $statement->fetch(PDO::FETCH_ASSOC); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } + + /** + * @return array|void + */ + public function create() + { + $tokenPrefix = uniqid(); + $result['tokenPrefix'] = $tokenPrefix; + try { + $key = bin2hex(random_bytes(24)); + $result['key'] = $key; + } catch (\Exception $e) { + echo $e->getMessage() . PHP_EOL; + exit(1); + } + $token = password_hash($tokenPrefix . '.' . $key, PASSWORD_ARGON2ID); + + //print() + $statement = " + INSERT INTO user (api_token_prefix, api_token) + VALUES (:token_prefix, :token)"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam(':token_prefix', $tokenPrefix); + $statement->bindParam(':token', $token); + $statement->execute(); + $result['row'] = $this->dbConnection->lastInsertId(); + return $result; + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } + + + /** + * @param $id + * + * @return void + */ + public function delete($id) + { + $statement = " + DELETE FROM user + WHERE id = :id"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam('id', $id); + $statement->execute(); + return $statement->rowCount(); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } + + +} \ No newline at end of file diff --git a/bindAPI/src/Controller/DatabaseConnection.php b/bindAPI/src/Controller/DatabaseConnection.php new file mode 100644 index 0000000..63c156c --- /dev/null +++ b/bindAPI/src/Controller/DatabaseConnection.php @@ -0,0 +1,44 @@ +dbConnection = new PDO( + dsn: "mysql:host=$dbHost;port=$dbPort;charset=utf8mb4;dbname=$dbDatabase", + username: $dbUser, + password: $dbPassword + + ); + } catch (PDOException $exception) { + exit($exception->getMessage()); + } + } + + + /** + * @return \PDO + */ + public function getConnection(): PDO + { + return $this->dbConnection; + } +} \ No newline at end of file diff --git a/bindAPI/src/Controller/DomainController.php b/bindAPI/src/Controller/DomainController.php new file mode 100644 index 0000000..6b6047d --- /dev/null +++ b/bindAPI/src/Controller/DomainController.php @@ -0,0 +1,172 @@ +dbConnection = $dbConnection; + } + + /** + * @return array|false + */ + public function findAll(): bool|array + { + $statement = " + SELECT id, name, a, aaaa + FROM domains"; + + try { + $statement = $this->dbConnection->query($statement); + return $statement->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + exit($e->getMessage()); + } + } + + + /** + * @param String $name + * + * @return array|false + */ + public function findByName(String $name): bool|array + { + $statement = " + SELECT id, name, a, aaaa + FROM domains + WHERE name = :name"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam(':name', $name); + $statement->execute(); + + return $statement->fetchAll(PDO::FETCH_ASSOC); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } + + /** + * @param Int $id + * + * @return array|false + */ + public function findByID(Int $id): bool|array + { + $statement = " + SELECT id, name, a, aaaa + FROM domains + WHERE id = :id"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam(':id', $id); + $statement->execute(); + + return $statement->fetchAll(PDO::FETCH_ASSOC); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } + + + /** + * @param String $name + * @param String $a + * @param String $aaaa + * + * @return int + */ + public function insert(String $name, String $a, String $aaaa): int + { + // TODO create zone file and include + $statement = " + INSERT INTO domains (name, a, aaaa) + VALUES (:name, :a, :aaaa)"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam(':name', $name); + $statement->bindParam(':a', $a); + $statement->bindParam(':aaaa', $aaaa); + $statement->execute(); + return $statement->rowCount(); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + + // TODO + // create zone file + // add zone file to include file + } + + + /** + * @param Int $id + * @param String $name + * @param String $a + * @param String $aaaa + * + * @return int|void + */ + public function update(Int $id, String $name, String $a, String $aaaa) + { + // TODO UPDATE Zone file + $statement = " + UPDATE domains SET + name = :name, + a = :a, + aaaa = :aaaa + WHERE id = :id"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam('id', $id); + $statement->bindParam('name', $name); + $statement->bindParam('a', $a); + $statement->bindParam('aaaa', $aaaa); + $statement->execute(); + return $statement->rowCount(); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + + // TODO + // recreate zone file + } + + + /** + * @param $id + * + * @return int + */ + public function delete($id): int + { + // TODO delete zone file and include + $statement = " + DELETE FROM domains + WHERE id = :id"; + + try { + $statement = $this->dbConnection->prepare($statement); + $statement->bindParam('id', $id); + $statement->execute(); + return $statement->rowCount(); + } catch (\PDOException $e) { + exit($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/bindAPI/src/Controller/RequestController.php b/bindAPI/src/Controller/RequestController.php new file mode 100644 index 0000000..8f93c54 --- /dev/null +++ b/bindAPI/src/Controller/RequestController.php @@ -0,0 +1,244 @@ +dbConnection = $dbConnection; + $this->requestMethod = strtoupper($requestMethod); + $this->uri = $uri; + + $this->domainController = new DomainController($dbConnection); + + } + + public function processRequest() + { + $result = Array(); + + if (empty($this->uri[2]) || $this->uri[2] != 'domains') { + $this->status = "404 Not Found"; + $this->message = "Endpoint not found."; + } else { + if ($this->checkPassword()) { + switch ($this->requestMethod) { + case 'GET': + if (empty($this->uri[3])) { + $result = $this->domainController->findAll(); + } else { + if (strtolower($this->uri[3]) == "check") { + $result = $this->checkDomains(); + } else { + $result = $this->domainController->findByName($this->uri[3]); + } + } + break; + case 'POST': + $name = $_POST['name'] ?? ""; + $a = $_POST['a'] ?? ""; + $aaaa = $_POST['aaaa'] ?? ""; + if (empty($name)) { + $this->status = "400 Bad Request"; + $this->message = "A name is required"; + break; + } + if (empty($a) && empty($aaaa)) { + $this->status = "400 Bad Request"; + $this->message = "At least one IP address is required."; + break; + } + + if($this->domainController->findByName($name)) { + $this->status = "400 Bad request"; + $this->message = "Domain: $name already exists."; + } else { + $dcResult = $this->domainController->insert($name, $a, $aaaa); + $this->status = "201 Created"; + $this->message = $dcResult; + } + break; + case 'PUT': + $putData = fopen('php://input', 'r'); + $data = fread($putData, 512); + + $params = explode( '&', $data); + + foreach ($params as $param) { + [$key, $value] = explode('=', $param); + $put[$key] = $value; + } + $id = $put['id'] ?? 0; + $name = $put['name'] ?? ""; + $a = $put['a'] ?? ""; + $aaaa = $put['aaaa'] ?? ""; + + if ($id == 0) { + $this->status = "400 Bad Request"; + $this->message = "An ID is required"; + break; + } + if(!$this->domainController->findByID($id)) { + $this->status = "400 Bad request"; + $this->message = "Domain with ID : $id doesn't exist."; + break; + } + if (empty($name)) { + $this->status = "400 Bad Request"; + $this->message = "A name is required"; + break; + } + if (empty($a) && empty($aaaa)) { + $this->status = "400 Bad Request"; + $this->message = "At least one IP address is required."; + break; + } + + $dcResult = $this->domainController->update($id, $name, $a, $aaaa); + $this->status = "201 Updated"; + $this->message = $dcResult; + + break; + case "DELETE": + $deleteData = fopen('php://input', 'r'); + $data = fread($deleteData, 512); + + $params = explode( '&', $data); + + foreach ($params as $param) { + [$key, $value] = explode('=', $param); + $delete[$key] = $value; + } + + $id = $delete['id'] ?? 0; + + if ($id == 0) { + $this->status = "400 Bad Request"; + $this->message = "A valid ID is required."; + break; + } + + if(!$this->domainController->findByID($id)) { + $this->status = "400 Bad Request"; + $this->message = "There is no domain with ID $id."; + break; + } + $this->domainController->delete($id); + $this->status = "204 No content."; + $this->message = "The domain $id has been deleted."; + break; + default: + $this->status = "400 Bad Request"; + $this->message = "unknown request method: $this->requestMethod"; + } + } + + } + + if (!empty($result)) { + echo json_encode($result); + } else { + echo json_encode([ + 'status' => $this->status ?? "Error: No status", + 'message' => $this->message ?? "Error: No message." + ]); + } + + } + + + /** + * @return array + */ + function checkDomains(): array + { + $errors = Array(); + $domains = $this->domainController->findAll(); + + // check for included main file in /etc/bind/named.conf.local + // it needs to include "/etc/bind/local.zones"; + + $localZoneFile = '/etc/bind/local.zones'; + $localZonesDir = '/etc/bind/zones/'; + $namedConfLocalFile = '/etc/bind/named.conf.local'; + + if ($namedConfLocal = file_get_contents($namedConfLocalFile)) { + if (!str_contains($namedConfLocal, $localZoneFile)) { + $errors[] = "$localZoneFile needs to be included in $namedConfLocalFile."; + } + } else { + $errors[] = "No access to '$namedConfLocalFile'. Please check permissions"; + return $errors; + } + + if (!fileperms($localZoneFile)) { + $errors[] = "No access to $localZoneFile. Please check permissions."; + return $errors; + } + + $localZones = file_get_contents($localZoneFile); + + + foreach($domains as $domain) { + if(!str_contains($localZones, $domain['name'])) { + $errors[] = $domain['name'] . " is missing in '$localZoneFile'"; + } + + $zoneFile = $localZonesDir . $domain['name']; + + if (!file_exists($zoneFile)) { + $errors[] = "Missing zone file for $zoneFile. Update zone to create it"; + } + } + + if (empty($errors)) { + return [ + 'status' => "200 domains healthy.", + 'message' => "All domains ar setup." + ]; + } else { + return $errors; + } + } + + /** + * @return bool + */ + public function checkPassword(): bool + { + $headers = array_change_key_case(getallheaders(), CASE_UPPER); + $apiKey = $headers['X-API-KEY'] ?? ""; + + if (empty($apiKey)) { + $this->status = "401 Unauthorized"; + $this->message = "API key is missing."; + return false; + } else { + [$prefix,] = explode('.', $apiKey); + $apiUsers = new ApiUsers($this->dbConnection); + $apiResult = $apiUsers->findByPrefix($prefix); + $storedHash = $apiResult['api_token']; + + if (!password_verify($apiKey, $storedHash)) { + $this->status = "401 Unauthorized"; + $this->message = "API key mismatch."; + return false; + } + } + return true; + } + +}