opengnsys/admin/WebConsole/rest/common.php

760 lines
26 KiB
PHP

<?php
/**
* @file index.php
* @brief OpenGnsys REST API: common functions and routes
* @warning All input and output messages are formatted in JSON.
* @note Some ideas are based on article "How to create REST API for Android app using PHP, Slim and MySQL" by Ravi Tamada, thanx.
* @license GNU GPLv3+
* @author Ramón M. Gómez, ETSII Univ. Sevilla
* @version 1.1.0 - First version
* @date 2016-11-17
*/
// Common constants.
define('REST_LOGFILE', '/opt/opengnsys/log/rest.log');
define('VERSION_FILE', '/opt/opengnsys/doc/VERSION.json');
// Set time zone.
if (function_exists("date_default_timezone_set")) {
if (exec("timedatectl status | awk '/Time zone/ {print $3}'", $out, $err)) {
date_default_timezone_set($out[0]);
}
}
// Common functions.
/**
* @brief Function to write a line into log file.
* @param string message Message to log.
* warning Line format: "Date: ClientIP: UserId: Status: Method Route: Message"
*/
function writeRestLog($message = "") {
global $userid;
if (is_writable(REST_LOGFILE)) {
$app = \Slim\Slim::getInstance();
file_put_contents(REST_LOGFILE, date(DATE_ISO8601) .": " .
$_SERVER['REMOTE_ADDR'] . ": " .
(isset($userid) ? $userid : "-") . ": " .
$app->response->getStatus() . ": " .
$app->request->getMethod() . " " .
$app->request->getPathInfo() . ": $message\n",
FILE_APPEND);
}
}
/**
* @brief Compose JSON response.
* @param int status Status code for HTTP response.
* @param array response Response data.
* @param int opts Options to encode JSON data.
* @return string JSON response.
*/
function jsonResponse($status, $response, $opts=0) {
$app = \Slim\Slim::getInstance();
// HTTP status code.
$app->status($status);
// Content-type HTTP header.
$app->contentType('application/json; charset=utf-8');
// JSON response.
echo json_encode($response, $opts);
}
/**
* @brief Print immediately JSON response to continue processing.
* @param int status Status code for HTTP response.
* @param array response Response data.
* @param int opts Options to encode JSON data.
* @return string JSON response.
*/
function jsonResponseNow($status, $response, $opts=0) {
// Compose headers and content.
ignore_user_abort();
http_response_code((int)$status);
header('Content-type: application/json; charset=utf-8');
ob_start();
echo json_encode($response, $opts);
$size = ob_get_length();
header("Content-Length: $size");
// Print content.
ob_end_flush();
flush();
session_write_close();
}
/**
* @brief Validate API key included in "Authorization" HTTP header.
* @return string JSON response on error.
*/
function validateApiKey() {
global $cmd;
global $userid;
$response = [];
$app = \Slim\Slim::getInstance();
// Read Authorization HTTP header.
if (! empty($_SERVER['HTTP_AUTHORIZATION'])) {
// Assign user id. that match this key to global variable.
$apikey = htmlspecialchars($_SERVER['HTTP_AUTHORIZATION']);
$cmd->texto = "SELECT idusuario
FROM usuarios
WHERE apikey='$apikey' LIMIT 1";
$rs=new Recordset;
$rs->Comando=&$cmd;
if ($rs->Abrir()) {
$rs->Primero();
if (!$rs->EOF){
// Fetch user id.
$userid = $rs->campos["idusuario"];
} else {
// Credentials error.
$response['message'] = 'Login failed, incorrect credentials';
jsonResponse(401, $response);
$app->stop();
}
$rs->Cerrar();
} else {
// Database error.
$response['message'] = "An error occurred, please try again";
jsonResponse(500, $response);
}
} else {
// Error: missing API key.
$response['message'] = 'Missing API key';
jsonResponse(400, $response);
$app->stop();
}
}
/**
* @brief Check if parameter is set and print error messages if empty.
* @param string param Parameter to check.
* @return boolean "false" if parameter is null, otherwise "true".
*/
function checkParameter($param) {
$response = [];
if (isset($param)) {
return true;
} else {
// Print error message.
$response['message'] = 'Parameter not found';
jsonResponse(400, $response);
return false;
}
}
/**
* @brief Check if all parameters are positive integer numbers.
* @param int id ... Identificators to check (variable number of parameters).
* @return boolean "true" if all ids are int>0, otherwise "false".
*/
function checkIds() {
$opts = ['options' => ['min_range' => 1]]; // Check for int>0
foreach (func_get_args() as $id) {
if (filter_var($id, FILTER_VALIDATE_INT, $opts) === false) {
return false;
}
}
return true;
}
/**
* @brief Show custom message for "not found" error (404).
*/
$app->notFound(
function() {
$response['message'] = 'REST route not found';
jsonResponse(404, $response);
}
);
/**
* @brief Hook to write an error log message and a REST exit log message if debug is enabled.
* @warning Error message will be written in web server's error file.
* @warning REST message will be written in REST log file.
*/
$app->hook('slim.after', function() use ($app) {
if ($app->response->getStatus() != 200 ) {
// Compose error message (truncating long lines).
$app->log->error(date(DATE_ISO8601) . ': ' .
$app->getName() . ': ' .
$_SERVER['REMOTE_ADDR'] . ": " .
(isset($userid) ? $userid : "-") . ": " .
$app->response->getStatus() . ': ' .
$app->request->getMethod() . ' ' .
$app->request->getPathInfo() . ': ' .
substr($app->response->getBody(), 0, 100));
}
if ($app->settings['debug'])
writeRestLog(substr($app->response->getBody(), 0, 30));
}
);
// Common routes.
/**
* @brief Get general server information
* @note Route: /info, Method: GET
* @return string JSON object with basic server information (version, services, etc.)
*/
$app->get('/info', function() {
$hasOglive = false;
$response = new \stdClass;
// Reading version file.
$data = json_decode(@file_get_contents(VERSION_FILE));
if (isset($data->project)) {
$response = $data;
} else {
$response->project = 'OpenGnsys';
}
// Getting actived services.
@$services = parse_ini_file('/etc/default/opengnsys');
$response->services = [];
if (@$services["RUN_OGADMSERVER"] === "yes") {
array_push($response->services, "server");
$hasOglive = true;
}
if (@$services["RUN_OGADMREPO"] === "yes") array_push($response->services, "repository");
if (@$services["RUN_BTTRACKER"] === "yes") array_push($response->services, "tracker");
// Reading installed ogLive information file.
if ($hasOglive === true) {
$data = json_decode(@file_get_contents('/opt/opengnsys/etc/ogliveinfo.json'));
if (isset($data->oglive)) {
$response->oglive = $data->oglive;
}
}
jsonResponse(200, $response);
}
);
function backupConfig() {
$get_command = 'config-get';
$get_output = executeCurlCommand($get_command);
if ($get_output == false && $get_output[0]["result"] != 0) {
throw new Exception('Error al obtener la configuración actual de Kea DHCP');
}
$config_text = json_encode($get_output[0]['arguments']);
$configurationParsed = str_replace('\\', '', $config_text);
$backup_dir = '/opt/opengnsys/etc/kea/backup';
if (!is_dir($backup_dir)) {
throw new Exception('El directorio de backup no existe');
}
if (!is_writable($backup_dir)) {
throw new Exception('El directorio de backup no tiene permisos de escritura');
}
$backup_files = glob($backup_dir . '/*.conf');
if (count($backup_files) >= 10) {
usort($backup_files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
echo $backup_files[0];
unlink($backup_files[0]);
}
$backup_file = $backup_dir . '/' . date('Y-m-d_H-i-s') . '.conf';
if (!file_put_contents($backup_file, $configurationParsed)) {
throw new Exception('Error al guardar la configuración de backup');
}
}
function initializeReservations($config) {
if (!isset($config[0]['arguments']['Dhcp4']['reservations'])) {
$config[0]['arguments']['Dhcp4']['reservations'] = [];
$set_output = executeCurlCommand('config-set', $config);
if ($set_output == false && $set_output[0]["result"] != 0) {
throw new Exception('Error al establecer la configuración de Kea DHCP');
}
}
return $config;
}
function executeCurlCommand($command, $arguments = null, $create_backup = true) {
$apiUrl = 'http://localhost:8000/';
if ($command == 'config-get' || $command == 'config-write') {
$requestData = array(
'command' => $command,
'service' => array('dhcp4'),
);
} elseif ($command == 'config-set' || $command == 'config-test') {
$requestData = array(
'command' => $command,
'service' => array('dhcp4'),
'arguments' => $arguments,
);
} else {
return "Error: Comando no válido";
}
if (($command == 'config-set' || $command == 'config-write') && $create_backup) {
backupConfig();
}
$jsonData = json_encode($requestData);
try{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_POST, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$output = json_decode($response, true);
if ($httpCode >= 200 && $httpCode < 300) {
if($command == 'config-get'){
$output = initializeReservations($output);
}
return $output;
} else {
$error = curl_error($ch);
throw new Exception($error);
//return false;
}
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* @brief Get configuration of kea dhcp server
* @note Route: /dhcp, Method: GET
* @return string JSON object with all kea configuration
*/
$app->get('/dhcp', function() {
try{
$response = executeCurlCommand('config-get');
if(!$response) {
$responseError = 'Error: No se pudo acceder al archivo dhcpd.conf';
jsonResponse(400, $responseError);
}else{
$result_code = $response[0]["result"];
if ($result_code == 0){
$arrayDhcp4 = $response[0]['arguments'];
//outputEscaped = json_encode($arrayDhcp4 , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
//cho $outputEscaped;
//echo $output;
jsonResponse(200, $arrayDhcp4 , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}else{
$responseError = "Error kea configuration invalid: " . $responseData[0]["text"];
jsonResponse(400, $responseError);
}
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
});
/**
* @brief Get hosts from kea configuration
* @note Route: /dhcp/hosts, Method: GET
* @return string JSON object with all hosts in kea configuration
*/
$app->get('/dhcp/hosts', function() {
try{
$response = executeCurlCommand('config-get');
if(!$response) {
$responseError = 'Error: No se pudo acceder al archivo dhcpd.conf';
jsonResponse(400, $response);
}else{
$result_code = $response[0]["result"];
if ($result_code == 0){
$arrayReservations = $response[0]['arguments']['Dhcp4']['reservations'];
jsonResponse(200, $arrayReservations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}else{
$responseError = "Error kea configuration invalid: " . $responseData[0]["text"];
jsonResponse(400, $responseError);
}
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
});
/**
* @brief Update kea configuration
* @note Route: /dhcp/save, Method: POST
* @param string configurationText
* @return string JSON object with all hosts in kea configuration
*/
$app->post('/dhcp/save', function() use ($app) {
try {
$input = json_decode($app->request()->getBody());
$configurationText = htmlspecialchars($input->configurationText);
} catch (Exception $e) {
$response["message"] = $e->getMessage();
jsonResponse(400, $response);
$app->stop();
}
$configurationParsed = str_replace(' ', '', $configurationText);
$configurationParsed = str_replace("\n", '', $configurationParsed);
$configurationParsed = str_replace('&quot;', '"', $configurationParsed);
$configuration = json_decode($configurationParsed);
if ($configuration === null && json_last_error() !== JSON_ERROR_NONE) {
$responseError = "Error: El texto proporcionado no es un JSON válido.";
jsonResponse(400, $responseError);
}else{
try{
$responseTest = executeCurlCommand('config-test', $configuration);
if ($responseTest[0]["result"] == 0){
$responseSet = executeCurlCommand('config-set',$configuration);
if ($responseSet == false && $responseSet[0]["result"] != 0) {
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseSet[0]["text"];
jsonResponse(400, $responseError);
}else{
$responseSuccess = "Configuración cargada correctamente";
jsonResponse(200, $responseSuccess);
}
}else{
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseTest[0]["text"];
jsonResponse(400, $responseError);
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
}
});
$app->post('/dhcp/add',
function() use ($app) {
try {
$input = json_decode($app->request()->getBody());
$host = htmlspecialchars($input->host);
$macAddress = htmlspecialchars($input->macAddress);
$address = htmlspecialchars($input->address);
$nextServer = htmlspecialchars($input->nextServer);
} catch (Exception $e) {
$response["message"] = $e->getMessage();
jsonResponse(400, $response);
$app->stop();
}
try{
$response = executeCurlCommand('config-get');
$nuevoHost = [
"hostname" => $host,
"hw-address" => $macAddress,
"ip-address" => $address,
"next-server" => $nextServer
];
if (!isset($response[0]['arguments']['Dhcp4']['reservations'])) {
$response[0]['arguments']['Dhcp4']['reservations'] = [];
}
$existe = False;
$existe = array_reduce($response[0]['arguments']['Dhcp4']['reservations'], function ($existe, $reserva) use ($host) {
return $existe || ($reserva['hostname'] === $host);
});
if ($existe) {
$response = "Error: El host con el hostname '$host' ya existe en las reservaciones.";
jsonResponse(400, $response);
}else{
$response[0]['arguments']['Dhcp4']['reservations'][] = $nuevoHost;
$array_encoded = json_encode($response[0]['arguments']);
$configurationParsed = str_replace('\\', '', $array_encoded);
$configuration = json_decode($configurationParsed);
$responseTest = executeCurlCommand('config-test',$configuration);
if ($responseTest[0]["result"] == 0){
$responseSet = executeCurlCommand('config-set',$configuration);
if ($responseSet == false && $responseSet[0]["result"] != 0) {
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseSet[0]["text"];
jsonResponse(400, $responseError);
}else{
$responseSuccess = "Configuración cargada correctamente";
jsonResponse(200, $responseSuccess);
}
}else{
$responseError = "Error kea configuration invalid: " . $responseTest[0]["text"];
jsonResponse(400, $responseError);
}
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
});
$app->post('/dhcp/delete',
function() use ($app) {
try {
$input = json_decode($app->request()->getBody());
$host = htmlspecialchars($input->host);
} catch (Exception $e) {
$response["message"] = $e->getMessage();
jsonResponse(400, $response);
$app->stop();
}
try{
$response = executeCurlCommand('config-get');
$existe = False;
foreach ($response[0]['arguments']['Dhcp4']['reservations'] as $key => $reservation) {
if (isset($reservation['hostname']) && $reservation['hostname'] === $host) {
unset($response[0]['arguments']['Dhcp4']['reservations'][$key]);
$existe = True;
$response[0]['arguments']['Dhcp4']['reservations'] = array_values($response[0]['arguments']['Dhcp4']['reservations']);
break;
}
}
if ($existe){
$array_encoded = json_encode($response[0]['arguments']);
$configurationParsed = str_replace('\\', '', $array_encoded);
$configuration = json_decode($configurationParsed);
$responseTest = executeCurlCommand('config-test',$configuration);
if ($response[0]["result"] == 0){
$responseSet = executeCurlCommand('config-set',$configuration);
if ($responseSet == false && $responseSet[0]["result"] != 0) {
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseSet[0]["text"];
jsonResponse(400, $responseError);
}else{
$responseSuccess = "Configuración cargada correctamente";
jsonResponse(200, $responseSuccess);
}
}else{
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseTest[0]["text"];
jsonResponse(400, $responseError);
}
}else{
$responseError = "Error: El host con el hostname '$host' no existe en las reservaciones.";
jsonResponse(400, $responseError);
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
}
);
$app->post('/dhcp/update',
function() use ($app) {
try {
$input = json_decode($app->request()->getBody());
$host = htmlspecialchars($input->host);
$oldMacAddress = htmlspecialchars($input->oldMacAddress);
$oldAddress = htmlspecialchars($input->oldAddress);
$macAddress = htmlspecialchars($input->macAddress);
$address = htmlspecialchars($input->address);
$nextServer = htmlspecialchars($input->nextServer);
} catch (Exception $e) {
$response["message"] = $e->getMessage();
jsonResponse(400, $response);
$app->stop();
}
try{
$response = executeCurlCommand('config-get');
$existe = False;
foreach ($response[0]['arguments']['Dhcp4']['reservations'] as &$reservation) {
if ($reservation['hw-address'] == $oldMacAddress && $reservation['ip-address'] == $oldAddress) {
$existe = True;
$reservation['hw-address'] = $macAddress;
$reservation['ip-address'] = $address;
$reservation['hostname'] = $host;
$reservation['next-server'] = $nextServer;
$array_encoded = json_encode($response[0]['arguments']);
$configurationParsed = str_replace('\\', '', $array_encoded);
$configuration = json_decode($configurationParsed);
$responseTest = executeCurlCommand('config-test',$configuration);
if ($responseTest[0]["result"] == 0){
$responseSet = executeCurlCommand('config-set',$configuration);
if ($responseSet == false && $responseSet[0]["result"] != 0) {
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseSet[0]["text"];
jsonResponse(400, $responseError);
}else{
$responseSuccess = "Configuración cargada correctamente";
jsonResponse(200, $responseSuccess);
}
}else{
$responseError = "Error al guardar la configuración en Kea DHCP: " . $responseTest[0]["text"];
jsonResponse(400, $responseError);
}
}
}
if(!$existe ){
$responseError = "Error: La IP ".$oldAddress." y la MAC " .$oldMacAddress. " no existe en las reservaciones.";
jsonResponse(400, $responseError);
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
});
$app->post('/dhcp/apply',
function() use ($app) {
try {
$response = executeCurlCommand('config-write');
if ($response[0]["result"] == 0){
jsonResponse(200, $response);
}else{
$responseError = "Error al escribir la configuración en Kea DHCP: " . $response[0]["text"];
jsonResponse(400, $responseError);
}
} catch (Exception $e) {
$responseError = "Error al escribir la configuración en Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
}
);
/**
* @brief Restore kea configuration.
* @note Route: /dhcp/backup, Method: POST
* @return string Result of operation.
* @note All modifications in kea configuration is stored"/opt/opengnsys/etc/kea/backup" directory.
*/
$app->post('/dhcp/backup', function() use ($app) {
$backup_dir = '/opt/opengnsys/etc/kea/backup';
$backup_files = glob($backup_dir . '/*.conf');
if (empty($backup_files)) {
$response = "No se encontraron archivos de backup";
jsonResponse(400, $response);
}
usort($backup_files, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$backup_file = reset($backup_files);
$config = file_get_contents($backup_file);
$configuration = json_decode($config);
$test_command = 'config-test';
$test_output = executeCurlCommand($test_command, $configuration);
if ($test_output == false && $test_output[0]["result"] != 0) {
$responseError = "Error al comprobar la configuración de Kea";
jsonResponse(400, $responseError);
}else{
$set_command = 'config-set';
$set_output = executeCurlCommand($set_command, $configuration, false);
if ($set_output == false && $set_output[0]["result"] != 0) {
$responseError = "Error al guardar la última configuración de Kea" . $set_output[0]["text"];
jsonResponse(400, $responseError);
}else{
unlink($backup_file);
$responseSuccess = "última configuración cargada correctametne";
jsonResponse(200, $responseSuccess);
}
}
});
/**
* @brief Upload kea configuration file.
* @note Route: /dhcp/upload, Method: POST
* @param File file.
* @return string Route of stored file.
*/
$app->post('/dhcp/upload', function() use ($app) {
if (!isset($_FILES['file_input_name'])) {
$responseError = "No se ha subido ningún archivo";
jsonResponse(400, $responseError);
return;
}
$file_path = $_FILES['file_input_name']['tmp_name'];
$json_data = file_get_contents($file_path);
$config_data = json_decode($json_data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$responseError = "El archivo JSON subido no está correctamente escrito";
jsonResponse(400, $responseError);
}
try{
$test_command = 'config-test';
$test_output = executeCurlCommand($test_command, $config_data);
if ($test_output == false && $test_output[0]["result"] != 0) {
$responseError = "La configuración no es válida";
jsonResponse(400, $responseError);
}else{
$set_command = 'config-set';
$set_output = executeCurlCommand($set_command, $config_data);
if ($test_output == false && $test_output[0]["result"] != 0) {
$responseError = "Error al guardar la configuración en Kea DHCP";
jsonResponse(400, $responseError);
http_response_code(400);
}else{
$responseSuccess = "Configuración cargada correctamente en el fichero: " . $file_path;;
jsonResponse(200, $responseSuccess);
http_response_code(200);
}
}
}catch (Exception $e) {
$responseError = "Error al obtener la configuración de Kea DHCP: " . $e->getMessage();
jsonResponse(400, $responseError);
}
});
/**
* @brief Get the server status
* @note Route: /status, Method: GET
* @return string JSON object with all data collected from server status (RAM, %CPU, etc.).
*/
$app->get('/status', function() {
$response = [];
// Getting memory and CPU information.
exec("awk '$1~/Mem/ {print $2}' /proc/meminfo",$memInfo);
$memInfo = array("total" => $memInfo[0], "used" => $memInfo[1]);
$cpuInfo = exec("awk '$1==\"cpu\" {printf \"%.2f\",($2+$4)*100/($2+$4+$5)}' /proc/stat");
$cpuModel = exec("awk -F: '$1~/model name/ {print $2}' /proc/cpuinfo");
$response["memInfo"] = $memInfo;
$response["cpu"] = array("model" => trim($cpuModel), "usage" => $cpuInfo);
jsonResponse(200, $response);
}
);