pull/812/merge
Matěj Kmínek 2020-01-16 08:12:22 +08:00 committed by GitHub
commit cdae50905b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 259 additions and 38 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

@ -390,7 +390,7 @@ namespace {
* @param string|bool $website_password Password website
* @param string|null $request_method Request method like GET, POST etc.
* @param string|null $post_field POST data
* @return string cURL result
* @return array ["result" => cURL result, "info" => cURL info array]
*/
function psm_curl_get(
$href,
@ -456,6 +456,8 @@ namespace {
}
$result = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if (defined('PSM_DEBUG') && PSM_DEBUG === true && psm_is_cli()) {
@ -466,7 +468,7 @@ namespace {
'==============END cURL Resul for: ' . $href . '===========================================' . PHP_EOL;
}
return $result;
return ["result" => $result, "info" => $info];
}
/**
@ -551,7 +553,7 @@ namespace {
// been more than a week since update, lets go
// update last check date
psm_update_conf('last_update_check', time());
$latest = psm_curl_get(PSM_UPDATE_URL);
$latest = psm_curl_get(PSM_UPDATE_URL)["result"];
// extract latest version from Github.
preg_match('/"tag_name":"[v](([\d][.][\d][.][\d])(-?\w*))"/', $latest, $latest);
// add latest version to database

View File

@ -84,6 +84,7 @@ $sm_lang = array(
'user_name' => 'Uživatelské jméno',
'password' => 'Heslo',
'password_repeat' => 'Stejné heslo (pro kontrolu)',
'api_hash' => 'API hash',
'password_leave_blank' => 'Ponechte prázdné pro ponechání beze změn.',
'level' => 'Oprávnění',
'level_10' => 'Administrátor',

View File

@ -102,6 +102,7 @@ $sm_lang = array(
'user_name' => 'Username',
'password' => 'Password',
'password_repeat' => 'Password repeat',
'api_hash' => 'API hash',
'password_leave_blank' => 'Leave blank to keep unchanged',
'level' => 'Level',
'level_10' => 'Administrator',

View File

@ -0,0 +1,86 @@
<?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() {
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

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

View File

@ -161,7 +161,8 @@ class UserController extends AbstractController
'pushover_key',
'pushover_device',
'telegram_id',
'email'
'email',
'api_hash'
);
if ($user_id == 0) {
@ -257,7 +258,8 @@ class UserController extends AbstractController
'pushover_key',
'pushover_device',
'telegram_id',
'email'
'email',
'api_hash'
);
$clean = array();
foreach ($fields as $field) {
@ -381,6 +383,7 @@ class UserController extends AbstractController
'label_user_name' => psm_get_lang('users', 'user_name'),
'label_password' => psm_get_lang('users', 'password'),
'label_password_repeat' => psm_get_lang('users', 'password_repeat'),
'label_api_hash' => psm_get_lang('users', 'api_hash'),
'label_level' => psm_get_lang('users', 'level'),
'label_level_description' => psm_get_lang('users', 'level_description'),
'label_mobile' => psm_get_lang('users', 'mobile'),

View File

@ -53,6 +53,12 @@ class Router
*/
protected $container;
/**
* Indication whether this request comes from API
* @var boolean
*/
private $isApi = false;
public function __construct()
{
$this->container = $this->buildServiceContainer();
@ -85,13 +91,19 @@ class Router
}
$this->buildTwigEnvironment();
$controller = $this->getController($mod, $controller);
$controller = $this->isApi ?
$this->getController($mod, $controller, true) :
$this->getController($mod, $controller);
$action = null;
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;
@ -116,16 +128,22 @@ class Router
* Get an instance of the requested controller.
* @param string $module_id
* @param string $controller_id if NULL, default controller will be used
* @param boolean $isApi
* @return \psm\Module\ControllerInterface
* @throws \InvalidArgumentException
*/
public function getController($module_id, $controller_id = null)
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])) {
@ -165,6 +183,10 @@ class Router
{
$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();
@ -200,6 +222,30 @@ class Router
}
}
/**
* Validate Api requets before heading to a controller
* @param \psm\Module\ControllerInterface $controller
* @param Request $request
* @throws \InvalidArgumentException
*/
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
@ -247,4 +293,15 @@ class Router
return $twig;
}
public function getIsApi()
{
return $this->isApi;
}
public function setIsApi($isApi)
{
$this->isApi = $isApi;
return $this;
}
}

View File

@ -156,6 +156,25 @@ 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.
*
@ -179,6 +198,28 @@ 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

@ -200,6 +200,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,

View File

@ -263,30 +263,22 @@ class StatusUpdater
$this->server['request_method'],
$this->server['post_field']
);
$this->header = $curl_result;
$this->header = $curl_result["result"];
$code = $curl_result["info"]["http_code"];
$redirectUrl = $curl_result["info"]["redirect_url"];
$this->rtime = (microtime(true) - $starttime);
// the first line would be the status code..
$status_code = strtok($curl_result, "\r\n");
// keep it general
// $code[2][0] = status code
// $code[3][0] = name of status code
$code_matches = array();
preg_match_all("/[A-Z]{2,5}\/\d(\.\d)?\s(\d{3})\s?(.*)/", $status_code, $code_matches);
if (empty($code_matches[0])) {
if (empty($code)) {
// somehow we dont have a proper response.
$this->error = 'TIMEOUT ERROR: no response from server';
$result = false;
} else {
$code = $code_matches[2][0];
$msg = $code_matches[3][0];
$allow_http_status = explode("|", $this->server['allow_http_status']);
// All status codes starting with a 4 or higher mean trouble!
if (substr($code, 0, 1) >= '4' && !in_array($code, $allow_http_status)) {
$this->error = "HTTP STATUS ERROR: " . $code . ' ' . $msg;
$this->error = "HTTP STATUS ERROR: " . $code;
$result = false;
} else {
$result = true;
@ -299,7 +291,7 @@ class StatusUpdater
($this->server['pattern_online'] == 'yes') ==
!preg_match(
"/{$this->server['pattern']}/i",
$curl_result
$this->header
)
) {
$this->error = "TEXT ERROR : Pattern '{$this->server['pattern']}' " .
@ -311,20 +303,11 @@ class StatusUpdater
// Check if the website redirects to another domain
if ($this->server['redirect_check'] == 'bad') {
$location_matches = array();
preg_match(
'/([Ll]ocation: )(https*:\/\/)(www.)?([a-zA-Z.:0-9]*)([\/][[:alnum:][:punct:]]*)/',
$curl_result,
$location_matches
);
if (!empty($location_matches)) {
$ip_matches = array();
preg_match(
'/(https*:\/\/)(www.)?([a-zA-Z.:0-9]*)([\/][[:alnum:][:punct:]]*)?/',
$this->server['ip'],
$ip_matches
);
if (strtolower($location_matches[4]) !== strtolower($ip_matches[3])) {
if (!empty($redirectUrl)) {
$redirectPieces = parse_url($redirectUrl);
$ipPieces = parse_url($this->server['ip']);
if (strtolower($redirectPieces['host']) !== strtolower($ipPieces['host'])) {
$this->error = "The IP/URL redirects to another domain.";
$result = false;
}
@ -335,7 +318,7 @@ class StatusUpdater
if ($this->server['header_name'] != '' && $this->server['header_value'] != '') {
$header_flag = false;
// Only get the header text if the result also includes the body
$header_text = substr($curl_result, 0, strpos($curl_result, "\r\n\r\n"));
$header_text = substr($this->header, 0, strpos($this->header, "\r\n\r\n"));
foreach (explode("\r\n", $header_text) as $i => $line) {
if ($i === 0 || strpos($line, ':') == false) {
continue; // We skip the status code & other non-header lines. Needed for proxy or redirects

View File

@ -13,6 +13,8 @@
{{ macro.input_field("password", "password", null, "password", label_password, edit_value_password, placeholder_password, "255") }}
<!-- Password repeat -->
{{ macro.input_field("password", "password_repeat", null, "password_repeat", label_password_repeat, edit_value_password_repeat, placeholder_password, "255") }}
<!-- API authentication hash -->
{{ macro.input_field("text", "api_hash", null, "api_hash", label_api_hash, edit_value_api_hash, placeholder_api_hash, "255") }}
<!-- Email -->
{{ macro.input_field("email", "email", null, "email", label_email, edit_value_email, null, "255") }}
<!-- Mobile -->