API interface and first basic API request:

https://phpservermonitor.com/api/?&mod=server_status&key=123456789
https://phpservermonitor.com/api/?&mod=server_status&key=123456789&action=detail&id=14

Created api folder and booter. User database row enhanced by api_auth field, to store user hashcode. Updated router, to hold information whether this is an api request and if is, load specific controller. Then use standard executeMethod to get proper JsonResponse.

First implemented API to get list of servers / one server detail
pull/812/head
Matej Kminek 2019-11-24 21:48:19 +01:00
parent a5312265f8
commit b7c1c4a29e
6 changed files with 381 additions and 165 deletions

43
api/index.php Normal file
View File

@ -0,0 +1,43 @@
<?php
/**
* PHP Server Monitor
* Monitor your servers and websites.
*
* This file is part of PHP Server Monitor.
* PHP Server Monitor is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PHP Server Monitor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PHP Server Monitor. If not, see <http://www.gnu.org/licenses/>.
*
* @package phpservermon
* @author Pepijn Over <pep@mailbox.org>
* @copyright Copyright (c) 2008-2017 Pepijn Over <pep@mailbox.org>
* @license http://www.gnu.org/licenses/gpl.txt GNU GPL v3
* @version Release: @package_version@
* @link http://www.phpservermonitor.org/
**/
require __DIR__.'/../src/bootstrap.php';
$router->setIsApi(true);
psm_no_cache();
$mod = psm_GET('mod');
try {
$router->run($mod);
} catch (\InvalidArgumentException $e) {
// invalid module, try the default one
// it that somehow also doesnt exist, we have a bit of an issue
// and we really have no reason catch it
$router->run(PSM_MODULE_DEFAULT);
}

View File

@ -0,0 +1,87 @@
<?php
/**
* PHP Server Monitor
* Monitor your servers and websites.
*
* This file is part of PHP Server Monitor.
* PHP Server Monitor is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PHP Server Monitor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PHP Server Monitor. If not, see <http://www.gnu.org/licenses/>.
*
* @package phpservermon
* @author Pepijn Over <pep@mailbox.org>
* @copyright Copyright (c) 2008-2017 Pepijn Over <pep@mailbox.org>
* @license http://www.gnu.org/licenses/gpl.txt GNU GPL v3
* @version Release: @package_version@
* @link http://www.phpservermonitor.org/
* */
/**
* Server module. List all servers, return list as JSON.
*/
namespace psm\Module\Server\Controller;
use psm\Service\Database;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Twig_Environment;
/**
* Description of ApiStatusController
*
* @author Matej Kminek <matej.kminek@attendees.eu>, 24. 11. 2019
*/
class ApiStatusController extends AbstractServerController {
/**
* Current server id
* @var int|\PDOStatement $server_id
*/
protected $server_id;
function __construct(Database $db, Twig_Environment $twig) {
parent::__construct($db, $twig);
$this->server_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$this->setActions(array('detail', 'list'), 'list');
}
/**
* Prepare the view template
*/
protected function executeList() {
$server = $this->getServers();
return new JsonResponse($server, Response::HTTP_OK);
}
/**
* Prepare the view template
*/
protected function executeDetail() {
var_dump($this->server_id);
if(empty($this->server_id)){
return new JsonResponse("Not found", Response::HTTP_NOT_FOUND);
}
$server = $this->getServers($this->server_id);
if (empty($server)) {
return $this->runAction('index');
}
return new JsonResponse($server, Response::HTTP_OK);
}
}

View File

@ -43,6 +43,7 @@ class ServerModule implements ModuleInterface {
'log' => __NAMESPACE__.'\Controller\LogController',
'status' => __NAMESPACE__.'\Controller\StatusController',
'update' => __NAMESPACE__.'\Controller\UpdateController',
'api.status' => __NAMESPACE__.'\Controller\ApiStatusController',
);
}

View File

@ -44,194 +44,240 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
*/
class Router {
/**
* Service container
* @var \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
protected $container;
/**
* Service container
* @var \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
protected $container;
public function __construct() {
$this->container = $this->buildServiceContainer();
/**
* Indication whether this request comes from API
* @var boolean
*/
private $isApi = false;
$mods = $this->container->getParameter('modules');
public function __construct() {
$this->container = $this->buildServiceContainer();
foreach ($mods as $mod) {
$mod_loader = $this->container->get($mod);
$mod_loader->load($this->container);
}
}
$mods = $this->container->getParameter('modules');
/**
* Run a module.
*
* The $mod param is in the format $module_$controller.
* If the "_$controller" part is omitted, it will attempt to load
* the controller with the same name as the module.
*
* @param string $mod
* @throws \InvalidArgumentException
* @throws \LogicException
*/
public function run($mod) {
if (strpos($mod, '_') !== false) {
list($mod, $controller) = explode('_', $mod);
} else {
$controller = $mod;
}
$this->buildTwigEnvironment();
foreach ($mods as $mod) {
$mod_loader = $this->container->get($mod);
$mod_loader->load($this->container);
}
}
$controller = $this->getController($mod, $controller);
$action = null;
/**
* Run a module.
*
* The $mod param is in the format $module_$controller.
* If the "_$controller" part is omitted, it will attempt to load
* the controller with the same name as the module.
*
* @param string $mod
* @throws \InvalidArgumentException
* @throws \LogicException
*/
public function run($mod) {
if (strpos($mod, '_') !== false) {
list($mod, $controller) = explode('_', $mod);
} else {
$controller = $mod;
}
$this->buildTwigEnvironment();
try {
$this->validateRequest($controller);
} catch (\InvalidArgumentException $ex) {
switch ($ex->getMessage()) {
case 'login_required':
$controller = $this->getController('user', 'login');
break;
case 'invalid_csrf_token':
case 'invalid_user_level':
default:
$controller = $this->getController('error');
$action = '401';
break;
}
}
$controller = $this->isApi ? $this->getController($mod, $controller, true) : $this->getController($mod, $controller);
$action = null;
$response = $controller->run($action);
try {
$this->validateRequest($controller);
} catch (\InvalidArgumentException $ex) {
switch ($ex->getMessage()) {
case 'invalid_api_key':
$controller = $this->getController('error');
$action = '403';
break;
case 'login_required':
$controller = $this->getController('user', 'login');
break;
case 'invalid_csrf_token':
case 'invalid_user_level':
default:
$controller = $this->getController('error');
$action = '401';
break;
}
}
if (!($response instanceof Response)) {
throw new \LogicException('Controller did not return a Response object.');
}
$response->send();
}
$response = $controller->run($action);
/**
* Get an instance of the requested controller.
* @param string $module_id
* @param string $controller_id if NULL, default controller will be used
* @return \psm\Module\ControllerInterface
* @throws \InvalidArgumentException
*/
public function getController($module_id, $controller_id = null) {
if ($controller_id === null) {
// by default, we use the controller with the same id as the module.
$controller_id = $module_id;
}
if (!($response instanceof Response)) {
throw new \LogicException('Controller did not return a Response object.');
}
$response->send();
}
/**
* Get an instance of the requested controller.
* @param string $module_id
* @param string $controller_id if NULL, default controller will be used
* @return \psm\Module\ControllerInterface
* @throws \InvalidArgumentException
*/
public function getController($module_id, $controller_id = null, $isApi = false) {
if ($controller_id === null) {
// by default, we use the controller with the same id as the module.
$controller_id = $module_id;
}
if ($isApi) {
// if this is an api request, serve api.controller
$controller_id = "api." . $controller_id;
}
$module = $this->container->get('module.'.$module_id);
$controllers = $module->getControllers();
if (!isset($controllers[$controller_id]) || !class_exists($controllers[$controller_id])) {
$controllers = $module->getControllers();
if (!isset($controllers[$controller_id]) || !class_exists($controllers[$controller_id])) {
throw new \InvalidArgumentException('Controller "'.$controller_id.'" is not registered or does not exist.');
}
$controller = new $controllers[$controller_id](
$this->container->get('db'),
$this->container->get('twig')
);
}
$controller = new $controllers[$controller_id](
$this->container->get('db'),
$this->container->get('twig')
);
if (!$controller instanceof \psm\Module\ControllerInterface) {
throw new \Exception('Controller does not implement ControllerInterface');
}
$controller->setContainer($this->container);
if (!$controller instanceof \psm\Module\ControllerInterface) {
throw new \Exception('Controller does not implement ControllerInterface');
}
$controller->setContainer($this->container);
return $controller;
}
return $controller;
}
/**
* Get service from container
* @param string $id
* @return mixed FALSE on failure, service otherwise
* @throws \InvalidArgumentException
*/
public function getService($id) {
return $this->container->get($id);
}
/**
* Get service from container
* @param string $id
* @return mixed FALSE on failure, service otherwise
* @throws \InvalidArgumentException
*/
public function getService($id) {
return $this->container->get($id);
}
/**
* Validate requets before heading to a controller
* @param \psm\Module\ControllerInterface $controller
* @throws \InvalidArgumentException
*/
protected function validateRequest(\psm\Module\ControllerInterface $controller) {
$request = Request::createFromGlobals();
/**
* Validate requets before heading to a controller
* @param \psm\Module\ControllerInterface $controller
* @throws \InvalidArgumentException
*/
protected function validateRequest(\psm\Module\ControllerInterface $controller) {
$request = Request::createFromGlobals();
if($this->isApi){
return $this->validateApiRequest($controller, $request);
}
if ($request->getMethod() == 'POST') {
// require CSRF token for all POST calls
$session = $this->container->get('user')->getSession();
$token_in = $request->request->get('csrf', '');
$csrf_key = $controller->getCSRFKey();
if ($request->getMethod() == 'POST') {
// require CSRF token for all POST calls
$session = $this->container->get('user')->getSession();
$token_in = $request->request->get('csrf', '');
$csrf_key = $controller->getCSRFKey();
if (empty($csrf_key)) {
if (!hash_equals($session->get('csrf_token'), $token_in)) {
throw new \InvalidArgumentException('invalid_csrf_token');
}
} else {
if (!hash_equals(
hash_hmac('sha256', $csrf_key, $session->get('csrf_token2')),
$token_in
)) {
throw new \InvalidArgumentException('invalid_csrf_token');
}
}
}
if (empty($csrf_key)) {
if (!hash_equals($session->get('csrf_token'), $token_in)) {
throw new \InvalidArgumentException('invalid_csrf_token');
}
} else {
if (!hash_equals(
hash_hmac('sha256', $csrf_key, $session->get('csrf_token2')),
$token_in
)) {
throw new \InvalidArgumentException('invalid_csrf_token');
}
}
}
// get min required level for this controller and make sure the user matches
$min_lvl = $controller->getMinUserLevelRequired();
// get min required level for this controller and make sure the user matches
$min_lvl = $controller->getMinUserLevelRequired();
if ($min_lvl < PSM_USER_ANONYMOUS) {
// if user is not logged in, load login module
if (!$this->container->get('user')->isUserLoggedIn()) {
throw new \InvalidArgumentException('login_required');
} elseif ($this->container->get('user')->getUserLevel() > $min_lvl) {
throw new \InvalidArgumentException('invalid_user_level');
}
}
}
if ($min_lvl < PSM_USER_ANONYMOUS) {
// if user is not logged in, load login module
if (!$this->container->get('user')->isUserLoggedIn()) {
throw new \InvalidArgumentException('login_required');
} elseif ($this->container->get('user')->getUserLevel() > $min_lvl) {
throw new \InvalidArgumentException('invalid_user_level');
}
}
}
private function validateApiRequest(\psm\Module\ControllerInterface $controller, Request $request) {
$result = $controller->getUser()->loginWithApiKey($request->query->get("key"));
if (!$result) {
throw new \InvalidArgumentException('invalid_api_key');
}
// get min required level for this controller and make sure the user matches
$min_lvl = $controller->getMinUserLevelRequired();
if ($min_lvl < PSM_USER_ANONYMOUS) {
// if user is not logged in, load login module
if ($this->container->get('user')->getUserLevel() > $min_lvl) {
throw new \InvalidArgumentException('invalid_user_level');
}
}
}
/**
* Build a new service container
* @return \Symfony\Component\DependencyInjection\ContainerBuilder
* @throws \InvalidArgumentException
*/
protected function buildServiceContainer() {
$builder = new ContainerBuilder();
$loader = new XmlFileLoader($builder, new FileLocator(PSM_PATH_CONFIG));
$loader->load('services.xml');
/**
* Build a new service container
* @return \Symfony\Component\DependencyInjection\ContainerBuilder
* @throws \InvalidArgumentException
*/
protected function buildServiceContainer() {
$builder = new ContainerBuilder();
$loader = new XmlFileLoader($builder, new FileLocator(PSM_PATH_CONFIG));
$loader->load('services.xml');
return $builder;
}
return $builder;
}
/**
* Prepare twig environment
* @return \Twig_Environment
*/
protected function buildTwigEnvironment() {
$twig = $this->container->get('twig');
$session = $this->container->get('user')->getSession();
if (!$session->has('csrf_token')) {
$session->set('csrf_token', bin2hex(random_bytes(32)));
}
if (!$session->has('csrf_token2')) {
$session->set('csrf_token2', random_bytes(32));
}
/**
* Prepare twig environment
* @return \Twig_Environment
*/
protected function buildTwigEnvironment() {
$twig = $this->container->get('twig');
$session = $this->container->get('user')->getSession();
if (!$session->has('csrf_token')) {
$session->set('csrf_token', bin2hex(random_bytes(32)));
}
if (!$session->has('csrf_token2')) {
$session->set('csrf_token2', random_bytes(32));
}
$twig->addFunction(
new \Twig_SimpleFunction(
'csrf_token',
function($lock_to = null) use ($session) {
if (empty($lock_to)) {
return $session->get('csrf_token');
}
return hash_hmac('sha256', $lock_to, $session->get('csrf_token2'));
}
)
);
$twig->addGlobal('direction_current', psm_get_lang('locale_dir'));
$twig->addGlobal('language_current', psm_get_lang('locale_tag'));
$twig->addGlobal('language', psm_get_lang('locale')[1]);
$twig->addFunction(
new \Twig_SimpleFunction(
'csrf_token',
function($lock_to = null) use ($session) {
if (empty($lock_to)) {
return $session->get('csrf_token');
}
return hash_hmac('sha256', $lock_to, $session->get('csrf_token2'));
}
)
);
$twig->addGlobal('direction_current', psm_get_lang('locale_dir'));
$twig->addGlobal('language_current', psm_get_lang('locale_tag'));
$twig->addGlobal('language', psm_get_lang('locale')[1]);
return $twig;
}
}
return $twig;
}
public function getIsApi() {
return $this->isApi;
}
public function setIsApi($isApi) {
$this->isApi = $isApi;
return $this;
}
}

View File

@ -147,6 +147,23 @@ class User {
return $query_user->fetchObject();
}
/**
* Search into database for the user data of api key specified as parameter
* @return object|boolean user data as an object if existing user
*/
public function getUserByApiKey($key) {
if(empty($key)){
return null;
}
// database query, getting all the info of the selected user
$query_user = $this->db_connection->prepare('SELECT * FROM '.PSM_DB_PREFIX.'users WHERE api_hash = :api_hash');
$query_user->bindValue(':api_hash', $key, \PDO::PARAM_STR);
$query_user->execute();
// get result row (as an object)
return $query_user->fetchObject();
}
/**
* Logs in with SESSION data.
*
@ -169,6 +186,27 @@ class User {
}
}
/**
* Logs in via the api key
* @return bool success state of cookie login
*/
public function loginWithApiKey($key) {
$apiKey = trim($key);
if (empty($apiKey)) {
return false;
}
$user = $this->getUserByApiKey($apiKey);
if (empty($user)) {
return false;
}
$this->setUserLoggedIn($user->user_id, true);
return true;
}
/**
* Logs in via the Cookie
* @return bool success state of cookie login

View File

@ -186,6 +186,7 @@ class Installer {
`password_reset_hash` char(40) DEFAULT NULL COMMENT 'user''s password reset code',
`password_reset_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the password reset request',
`rememberme_token` varchar(64) DEFAULT NULL COMMENT 'user''s remember-me cookie token',
`api_hash` varchar(255) DEFAULT NULL COMMENT 'user''s hash key to validate API requests',
`level` tinyint(2) unsigned NOT NULL DEFAULT '20',
`name` varchar(255) NOT NULL,
`mobile` varchar(15) NOT NULL,