From b7c1c4a29ec2a3dcb9ceb5dd8e11629281c6dc81 Mon Sep 17 00:00:00 2001 From: Matej Kminek Date: Sun, 24 Nov 2019 21:48:19 +0100 Subject: [PATCH] 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 --- api/index.php | 43 ++ .../Server/Controller/ApiStatusController.php | 87 ++++ src/psm/Module/Server/ServerModule.php | 1 + src/psm/Router.php | 376 ++++++++++-------- src/psm/Service/User.php | 38 ++ src/psm/Util/Install/Installer.php | 1 + 6 files changed, 381 insertions(+), 165 deletions(-) create mode 100644 api/index.php create mode 100644 src/psm/Module/Server/Controller/ApiStatusController.php diff --git a/api/index.php b/api/index.php new file mode 100644 index 00000000..ac5333bb --- /dev/null +++ b/api/index.php @@ -0,0 +1,43 @@ +. + * + * @package phpservermon + * @author Pepijn Over + * @copyright Copyright (c) 2008-2017 Pepijn Over + * @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); +} \ No newline at end of file diff --git a/src/psm/Module/Server/Controller/ApiStatusController.php b/src/psm/Module/Server/Controller/ApiStatusController.php new file mode 100644 index 00000000..cf910623 --- /dev/null +++ b/src/psm/Module/Server/Controller/ApiStatusController.php @@ -0,0 +1,87 @@ +. + * + * @package phpservermon + * @author Pepijn Over + * @copyright Copyright (c) 2008-2017 Pepijn Over + * @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 , 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); + } + +} diff --git a/src/psm/Module/Server/ServerModule.php b/src/psm/Module/Server/ServerModule.php index 44a5b06e..d78a156a 100644 --- a/src/psm/Module/Server/ServerModule.php +++ b/src/psm/Module/Server/ServerModule.php @@ -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', ); } diff --git a/src/psm/Router.php b/src/psm/Router.php index 140d95fb..c732aaee 100644 --- a/src/psm/Router.php +++ b/src/psm/Router.php @@ -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; - } -} \ No newline at end of file + return $twig; + } + + public function getIsApi() { + return $this->isApi; + } + + public function setIsApi($isApi) { + $this->isApi = $isApi; + return $this; + } + +} diff --git a/src/psm/Service/User.php b/src/psm/Service/User.php index 6906fd93..7a85e73f 100644 --- a/src/psm/Service/User.php +++ b/src/psm/Service/User.php @@ -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 diff --git a/src/psm/Util/Install/Installer.php b/src/psm/Util/Install/Installer.php index 7420c367..9de3ec6f 100644 --- a/src/psm/Util/Install/Installer.php +++ b/src/psm/Util/Install/Installer.php @@ -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,