requestMethod = strtoupper(string: $requestMethod); $dateFormat = "Y:m:d H:i:s"; $output = "%datetime% %channel%.%level_name% %message%\n"; // %context% %extra% $formatter = new LineFormatter(format: $output, dateFormat: $dateFormat); $stream = new StreamHandler(stream: dirname(path: __DIR__, levels: 2) . '/bindAPI.log'); $stream->setFormatter(formatter: $formatter); $this->log = new Logger(name: 'bindAPI'); $this->log->pushHandler(handler: $stream); if ($this->config['debug']) { $this->log->debug(message: 'RequestController::__construct'); } $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions([ DatabaseConnection::class => autowire()->constructorParameter(parameter: 'config', value: $this->config), DomainController::class => autowire() ->constructorParameter(parameter: 'config', value: $this->config) ->constructorParameter(parameter: 'log', value: $this->log), DomainRepository::class => autowire() ->constructorParameter(parameter: 'config', value: $this->config) ->constructorParameter(parameter: 'log', value: $this->log), DynDNSRepository::class => autowire() ->constructorParameter(parameter: 'config', value: $this->config) ->constructorParameter(parameter: 'log', value: $this->log), ]); $this->container = $containerBuilder->build(); $this->apiController = $this->container->get(name: ApiController::class); $this->apikeyRepository = $this->container->get(name: ApikeyRepository::class); $this->domainController = $this->container->get(name: DomainController::class); $this->domainRepository = $this->container->get(name: DomainRepository::class); $this->panelRepository = $this->container->get(name: PanelRepository::class); $this->dynDNSRepository = $this->container->get(name: DynDNSRepository::class); } /** * @return void */ #[OAT\Get( path : '/domains', operationId: 'getAllDomains', description: 'Returns a list of all domains on this server.', summary : 'Listing all domains.', // security: [ // 'Authorization' => [ // // "read:api" // ] // ], servers : [], tags : ['Domains'], 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.' )] )] private function handleAllDomainsGetRequest(): void { $domains = $this->domainRepository->findAll(); $resultDomain = []; foreach ($domains as $singleDomain) { $domain = [ 'id' => $singleDomain->getId(), 'name' => $singleDomain->getName(), 'panel' => $singleDomain->getPanel() ]; $resultDomain[] = $domain; } $this->result = $resultDomain; } /** */ private function handlePing() { if ($this->checkPassword()) { $this->header = '200 OK'; $this->status = json_encode(value: ['response' => 'pong']); } else { $this->header = '401 Unauthorized'; $this->status = json_encode(value: ['message' => 'API key is missing or invalid']); } } /** * @return void */ private function handleDomains(): void { if ($this->checkPassword()) { try { match ($this->requestMethod) { 'GET' => $this->handleDomainsGetRequest(), 'POST' => $this->handleDomainsPostRequest(), 'PUT' => $this->handleDomainsPutRequest(), 'DELETE' => $this->handleDomainsDeleteRequest() }; } catch (UnhandledMatchError) { $this->header = '400 Bad Request'; $this->status = '400 Bad Request'; $this->message = "unknown request method: $this->requestMethod"; } } } /** * @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"}} * } * ) * @return void */ #[ 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 processRequest() { $command = $this->uri[2]; if (empty($command) || !(($command == 'domains') || ($command == 'ping') || ($command == 'apidoc') || ($command == 'dyndns'))) { $this->header = '404 Not Found'; $this->status = "404 Not Found"; $this->message = "Endpoint not found."; } else { try { match ($command) { 'apidoc' => $this->apiDoc(), 'dyndns' => $this->handleDynDNS(), 'ping' => $this->handlePing(), 'domains' => $this->handleDomains(), }; } catch (UnhandledMatchError) { $this->header = '404 Bad Request'; $this->status = '404 Bad Request'; $this->message = 'Unknown path: ' . $command; } } if (!empty($this->header)) { header(header: $_SERVER['SERVER_PROTOCOL'] . ' ' . $this->header); } if (!empty($this->result)) { echo json_encode(value: $this->result); } elseif (!empty($this->status)) { echo $this->status; } 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); $apiKey = $headers['X-API-KEY'] ?? ''; if (empty($apiKey)) { $this->header = "401 Unauthorized"; $this->status = "401 Unauthorized"; $this->message = "API key is missing."; return false; } else { [$prefix,] = explode(separator: '.', string: $apiKey); if ($apiResult = $this->apikeyRepository->findByPrefix(prefix: $prefix)) { $storedHash = $apiResult->getApiToken(); if (!password_verify(password: $apiKey, hash: $storedHash)) { $this->header = "401 Unauthorized"; $this->status = "401 Unauthorized"; $this->message = "API key mismatch."; return false; } } else { $this->header = "401 Unauthorized"; $this->status = "401 Unauthorized"; $this->message = "Invalid API key."; return false; } } return true; } /** * @return void */ private function handleDomainsGetRequest(): void { $name = $this->uri[3] ?? ''; if ($name == 'name') { if ($result = $this->domainRepository->findByName(name: $this->uri[4])) { $domain = [ 'id' => $result->getId(), 'name' => $result->getName(), 'panel' => $result->getPanel() ]; $this->result = $domain; } else { $this->header = "404 Not Found "; $this->status = "404 Not Found "; $this->message = "The specified domain was not found."; } } else { if (empty($name)) { $this->handleAllDomainsGetRequest(); } else { $id = intval(value: $name); if ($id > 0) { if ($result = $this->domainRepository->findById(id: $id)) { $domain = [ 'id' => $result->getId(), 'name' => $result->getName(), 'panel' => $result->getPanel() ]; $this->result = $domain; } else { $this->header = "404 Not Found "; $this->status = "404 Not Found "; $this->message = "The specified domain was not found."; } } else { $this->header = "400 Bad request"; $this->status = "400 Not Found"; $this->message = "You need to supply an ID or user the /domain/name/ path."; } } } } /** * @return void */ private function handleDomainsPostRequest(): void { $name = $_POST['name'] ?? ''; $panel = $_POST['panel'] ?? ''; if (empty($name)) { $this->header = "400 Bad Request"; $this->status = "400 Bad Request"; $this->message = "A name is required"; } else { if (empty($panel)) { $this->header = "400 Bad Request"; $this->status = "400 Bad Request"; $this->message = "A panel ID is required."; } else { if ($this->domainRepository->findByName(name: $name)) { $this->header = "400 Bad request"; $this->status = "400 Bad request"; $this->message = "Domain: $name already exists."; } else { $domain = new Domain(name: $name, panel: $panel); if ($result = $this->domainRepository->insert(domain: $domain)) { $this->header = "201 Created"; $this->status = "201 Created"; $this->domainController->createSlaveZoneFile(domain: $domain); } else { $this->header = "500 Server error"; $this->status = "500 Server error"; } $this->message = $result; } } } } /** * @return void */ private function handleDomainsPutRequest(): void { $putData = fopen(filename: 'php://input', mode: 'r'); $data = fread(stream: $putData, length: 8192); $params = explode(separator: '&', string: $data); foreach ($params as $param) { [$key, $value] = explode(separator: '=', string: $param); $put[$key] = $value; } $id = $put['id'] ?? 0; $name = $put['name'] ?? ''; $panel = $put['panel'] ?? ""; if ($id == 0) { $this->header = "400 Bad Request"; $this->status = "400 Bad Request"; $this->message = "An ID is required"; } else { if (!$this->domainRepository->findByID(id: $id)) { $this->status = "404 Not Found"; $this->message = "Domain with ID : $id doesn't exist."; } else { $domain = new Domain(name: $name, panel: $panel, id: $id); $this->domainRepository->update(domain: $domain); $this->header = "201 Updated"; $this->status = "201 Updated"; $this->message = "201 Updated"; $this->domainController->createSlaveZoneFile(domain: $domain); } } } /** * @return void */ private function handleDomainsDeleteRequest(): void { $deleteData = fopen(filename: 'php://input', mode: 'r'); $data = fread(stream: $deleteData, length: 512); $params = explode(separator: '&', string: $data); foreach ($params as $param) { [$key, $value] = explode(separator: '=', string: $param); $delete[$key] = $value; } $id = $delete['id'] ?? 0; if ($id == 0) { $this->header = "400 Bad Request"; $this->status = "400 Bad Request"; $this->message = "You need to supply an ID."; } else { if (!$domain = $this->domainRepository->findByID(id: $id)) { $this->header = "400 Bad Request"; $this->status = "400 Bad Request"; $this->message = "There is no domain with ID $id."; } else { $this->domainRepository->delete(domain: $domain); $this->header = "204 No content."; $this->status = "204 No content."; $this->message = "The domain $id has been deleted."; } } } private function apiDoc() { //TODO forward to apidoc … } private function handleDynDNS() { if ($this->config['debug']) { $this->log->debug(message: 'handleDynDNS()'); } if ($this->checkPassword()) { $host = $this->uri[3] ?? ''; if (empty($host)) { $this->header = '400 Bad Request'; $this->status = '400 Bad Request'; } else { $a = $_POST['a'] ?? ''; $aaaa = $_POST['aaaa'] ?? ''; if (empty($a) && empty($aaaa)) { $address = $_SERVER['REMOTE_ADDR']; if (filter_var(value: $address, filter: FILTER_VALIDATE_IP, options: FILTER_FLAG_IPV6)) { $aaaa = $address; } else { $a = $address; } } if ($this->config['debug']) { $this->log->debug(message: 'a: ' . $a); $this->log->debug(message: 'aaaa: ' . $aaaa); } $domainName = $this->getDomain(host: $host); $hostName = str_replace(search: '.' . $domainName, replace: '', subject: $host); if (!$domain = $this->domainRepository->findByName(name: $domainName)) { $this->header = '404 Not Found'; $this->message = 'Domain ' . $domainName . ' not found'; } else { // check if address has changed if ($dynDNS = $this->dynDNSRepository->findByName(name: $host)) { if ($this->config['debug']) { $this->log->debug(message: 'found host: ' . $host); $this->log->debug(message: "a: $a"); $this->log->debug(message: "aaaa: $aaaa"); } $ipChanged = false; if (!empty($a)) { if ($a != $dynDNS->getA()) { if ($this->config['debug']) { $this->log->debug(message: $a . ' != ' . $dynDNS->getA()); } $dynDNS->setA(a: $a); $ipChanged = true; } } if (!empty($aaaa)) { if ($aaaa != $dynDNS->getAaaa()) { if ($this->config['debug']) { $this->log->debug(message: $aaaa . ' != ' . $dynDNS->getAaaa()); } $dynDNS->setAaaa(aaaa: $aaaa); $ipChanged = true; } } if ($ipChanged) { $this->dynDNSRepository->update(dynDNS: $dynDNS); } } else { $dynDNS = new DynDNS(name: $host, a: $a, aaaa: $aaaa); $this->dynDNSRepository->insert(dynDNS: $dynDNS); $ipChanged = true; } if ($ipChanged) { $panel = $this->panelRepository->findByName(name: $domain->getPanel()); if (!empty($panel->getAaaa())) { $domainData = $this->apiController->sendCommand( requestType: 'GET', serverName : $panel->getName(), versionIP : 6, apiKey : $panel->getApikey(), command : 'domains/name/' . $domainName, serverType : 'panel'); } else { $domainData = $this->apiController->sendCommand( requestType: 'GET', serverName : $panel->getName(), versionIP : 4, apiKey : $panel->getApikey(), command : 'domains/name/' . $domainName, serverType : 'panel'); } $domainDecodedData = json_decode(json: $domainData['data']); $domainID = $domainDecodedData->id; if (!empty($panel->getAaaa())) { $dnsData = $this->apiController->sendCommand( requestType: 'GET', serverName : $panel->getName(), versionIP : 6, apiKey : $panel->getApikey(), command : 'dns/' . $domainID, serverType : 'panel'); } else { $dnsData = $this->apiController->sendCommand( requestType: 'GET', serverName : $panel->getName(), versionIP : 4, apiKey : $panel->getApikey(), command : 'dns/' . $domainID, serverType : 'panel'); } $dnsDataDecoded = json_decode(json: $dnsData['data']); $soa = $dnsDataDecoded->records->soa; $others = $dnsDataDecoded->records->other; $hostFound = false; $updateHost = function (object $host) use ($hostName, $a, $aaaa, &$hostFound) { if ($host->host == $hostName) { $hostFound = true; if ($host->type == 'A') { if (!empty($a)) { $host->value = $a; } } else { if (!empty($aaaa)) { $host->value = $aaaa; } } } }; array_map(callback: $updateHost, array: $others); if ($hostFound) { $newDnsData = json_encode(value: [ 'records' => [ 'soa' => $soa, 'other' => $others ] ]); if (!empty($panel->getAaaa())) { $result = $this->apiController->sendCommand( requestType: 'PUT', serverName : $panel->getName(), versionIP : 6, apiKey : $panel->getApikey(), command : 'dns/' . $domainID, serverType : 'panel', body : json_decode(json: $newDnsData, associative: true) ); } else { $result = $this->apiController->sendCommand( requestType: 'PUT', serverName : $panel->getName(), versionIP : 4, apiKey : $panel->getApikey(), command : 'dns/' . $domainID, serverType : 'panel', body : json_decode(json: $newDnsData, associative: true) ); } if ($result['header'] == 200) { $this->header = '200 OK'; $this->message = 'DynDNS host successfully updated'; } } else { $this->header = '404 Not Found'; $this->message = 'Host ' . $hostName . ' not found'; } } else { $this->header = '204 No content'; $this->message = 'No content'; } } } } } /** * @param String $host * * @return string */ private function getDomain(string $host): string { $host = strtolower(string: trim(string: $host)); $count = substr_count(haystack: $host, needle: '.'); if ($count == 2) { if (strlen(string: explode(separator: '.', string: $host)[1]) > 3) { $host = explode(separator: '.', string: $host, limit: 2)[1]; } } elseif ($count > 2) { $host = $this->getDomain(host: explode(separator: '.', string: $host, limit: 2)[1]); } return $host; } }