diff --git a/AUTH.php b/AUTH.php new file mode 100644 index 0000000..b65e1c0 --- /dev/null +++ b/AUTH.php @@ -0,0 +1,4 @@ +selfDestruct(); \ No newline at end of file diff --git a/README.md b/README.md index acb2923..b33afbf 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ V2 build is going to be a complete overhaul of My idlers with the project being Despite what the name infers this self hosted web app isn't just for storing idling server information. By using a [YABs](https://github.com/masonr/yet-another-bench-script) output you can get disk & network speed values along with GeekBench 5 scores to do easier comparing and sorting. -[![Generic badge](https://img.shields.io/badge/version-1.4-blue.svg)](https://shields.io/) + +[![Generic badge](https://img.shields.io/badge/version-1.5-blue.svg)](https://shields.io/) ## 1.4 changes: **If you have version 1.3 already installed please run ```update1.3to1.4.sql```** diff --git a/calls.php b/calls.php index e26aed4..8cbf4e6 100644 --- a/calls.php +++ b/calls.php @@ -2,99 +2,108 @@ header('Access-Control-Allow-Origin: *'); header('Content-Type: application/json'); require_once('class.php'); -$idle = new idlers(); - -if ($_SERVER['REQUEST_METHOD'] === 'GET') { - if (isset($_GET['type'])) { - if ($_GET['type'] == 'server') { - echo $idle->serverData($_GET['id']); - } elseif ($_GET['type'] == 'search') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->searchResults($_GET['value']); - } elseif ($_GET['type'] == 'shared_hosting') { - echo $idle->sharedHostingData($_GET['id']); - } elseif ($_GET['type'] == 'domain') { - echo $idle->domainData($_GET['id']); - } elseif ($_GET['type'] == 'yabsModal') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->showYabsModal($_GET['id']);//Not used anymore. Still here for debugging - } elseif ($_GET['type'] == 'infoCard') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->infoCard();//Info card for the "info" tab - } elseif ($_GET['type'] == 'autocomplete') { - if ($_GET['input'] == 'location') { - $idle->locationsAutoCompleteGET($_GET['value']);//Auto complete locations input - } elseif ($_GET['input'] == 'provider') { - $idle->providersAutoCompleteGET($_GET['value']);//Auto complete providers input +$auth = new auth(); +$auth->sessionStartIfNone(); +if (isset($_SESSION['token'])) { + $idle = new idlers(); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if (isset($_GET['type'])) { + if ($_GET['type'] == 'server') { + echo $idle->serverData($_GET['id']); + } elseif ($_GET['type'] == 'search') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->searchResults($_GET['value']); + } elseif ($_GET['type'] == 'shared_hosting') { + echo $idle->sharedHostingData($_GET['id']); + } elseif ($_GET['type'] == 'domain') { + echo $idle->domainData($_GET['id']); + } elseif ($_GET['type'] == 'yabsModal') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->showYabsModal($_GET['id']);//Not used anymore. Still here for debugging + } elseif ($_GET['type'] == 'infoCard') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->infoCard();//Info card for the "info" tab + } elseif ($_GET['type'] == 'autocomplete') { + if ($_GET['input'] == 'location') { + $idle->locationsAutoCompleteGET($_GET['value']);//Auto complete locations input + } elseif ($_GET['input'] == 'provider') { + $idle->providersAutoCompleteGET($_GET['value']);//Auto complete providers input + } + } elseif ($_GET['type'] == 'view_more_modal') { + header('Content-Type: text/html; charset=utf-8'); + if ($_GET['value'] == 'server') { + $idle->viewMoreModal($_GET['id']);//View more details modal + } elseif ($_GET['value'] == 'shared') { + $idle->viewMoreSharedHostingModal($_GET['id']);//View more details modal + } elseif ($_GET['value'] == 'domain') { + $idle->viewMoreDomainModal($_GET['id']);//View more details modal + } + } elseif ($_GET['type'] == 'dns_search') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->getIpForDomain($_GET['hostname'], $_GET['dns_type']); + } elseif ($_GET['type'] == 'check_up') { + echo $idle->checkIsUp($_GET['host']); + } elseif ($_GET['type'] == 'object_cards') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->objectCards(); + } elseif ($_GET['type'] == 'object_tables') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->objectTables(); + } elseif ($_GET['type'] == 'compare_table') { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->compareTable($_GET['server1'], $_GET['server2']); } - } elseif ($_GET['type'] == 'view_more_modal') { - header('Content-Type: text/html; charset=utf-8'); - if ($_GET['value'] == 'server') { - $idle->viewMoreModal($_GET['id']);//View more details modal - } elseif ($_GET['value'] == 'shared') { - $idle->viewMoreSharedHostingModal($_GET['id']);//View more details modal - } elseif ($_GET['value'] == 'domain') { - $idle->viewMoreDomainModal($_GET['id']);//View more details modal - } - } elseif ($_GET['type'] == 'dns_search') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->getIpForDomain($_GET['hostname'], $_GET['dns_type']); - } elseif ($_GET['type'] == 'check_up') { - echo $idle->checkIsUp($_GET['host']); - } elseif ($_GET['type'] == 'object_cards') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->objectCards(); - } elseif ($_GET['type'] == 'object_tables') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->objectTables(); - } elseif ($_GET['type'] == 'compare_table') { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->compareTable($_GET['server1'], $_GET['server2']); } - } -} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (isset($_POST['order_form'])) { - header('Content-Type: text/html; charset=utf-8'); - echo $idle->orderTable($_POST['order_by']);//Returns order table - } else { - if (isset($_POST['action']) && $_POST['action'] == 'insert') {//From an insert 'type' form - $insert = new itemInsert($_POST); - if (isset($_POST['from_yabs'])) {//From add form YABs - $id = $insert->insertBasicWithYabs();//Insert basic data from form - $response_code = $insert->insertYabsData();//Insert YABs data from the form - if ($response_code != 1) { - header('Content-Type: text/html; charset=utf-8'); - $update = new itemUpdate(array('me_server_id' => $id)); + } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['order_form'])) { + header('Content-Type: text/html; charset=utf-8'); + echo $idle->orderTable($_POST['order_by']);//Returns order table + } else { + if (isset($_POST['action']) && $_POST['action'] == 'insert') {//From an insert 'type' form + $insert = new itemInsert($_POST); + if (isset($_POST['from_yabs'])) {//From add form YABs + $id = $insert->insertBasicWithYabs();//Insert basic data from form + $response_code = $insert->insertYabsData();//Insert YABs data from the form + if ($response_code != 1) { + header('Content-Type: text/html; charset=utf-8'); + $update = new itemUpdate(array('me_server_id' => $id)); + $update->deleteObjectData(); + echo $response_code; + exit; + } + } elseif (isset($_POST['manual'])) {//From add form manual + $insert->insertBasic(); + } elseif (isset($_POST['shared_hosting_form'])) {//From shared hosting form + $insert->insertSharedHosting(); + } elseif (isset($_POST['domain_form'])) {//From domain form + $insert->insertDomain(); + } + } elseif (isset($_POST['action']) && $_POST['action'] == 'update') { + $update = new itemUpdate($_POST); + if (isset($_POST['me_delete']) || isset($_POST['sh_me_delete']) || isset($_POST['d_me_delete'])) {//Delete object $update->deleteObjectData(); - echo $response_code; - exit; + } elseif ($_POST['type'] == 'server_modal_edit') {//Update the server info + $update->updateServerFromModal(); + $update->updateServerPricingFromModal(); + if (!is_null($_POST['me_yabs']) && !empty($_POST['me_yabs'])) { + $update->updateYabsData(); + } + } elseif ($_POST['type'] == 'shared_hosting_modal_edit') {//Update the shared hosting info + $update->updateSharedHostingFromModal(); + $update->updateSharedHostingPricingFromModal(); + } elseif ($_POST['type'] == 'domain_modal_edit') {//Update the domain info + $update->updateDomainFromModal(); + $update->updateDomainPricingFromModal(); } - } elseif (isset($_POST['manual'])) {//From add form manual - $insert->insertBasic(); - } elseif (isset($_POST['shared_hosting_form'])) {//From shared hosting form - $insert->insertSharedHosting(); - } elseif (isset($_POST['domain_form'])) {//From domain form - $insert->insertDomain(); - } - } elseif (isset($_POST['action']) && $_POST['action'] == 'update') { - $update = new itemUpdate($_POST); - if (isset($_POST['me_delete']) || isset($_POST['sh_me_delete']) || isset($_POST['d_me_delete'])) {//Delete object - $update->deleteObjectData(); - } elseif ($_POST['type'] == 'server_modal_edit') {//Update the server info - $update->updateServerFromModal(); - $update->updateServerPricingFromModal(); - if (!is_null($_POST['me_yabs']) && !empty($_POST['me_yabs'])) { - $update->updateYabsData(); - } - } elseif ($_POST['type'] == 'shared_hosting_modal_edit') {//Update the shared hosting info - $update->updateSharedHostingFromModal(); - $update->updateSharedHostingPricingFromModal(); - } elseif ($_POST['type'] == 'domain_modal_edit') {//Update the domain info - $update->updateDomainFromModal(); - $update->updateDomainPricingFromModal(); } + header('Location:index.php'); + die(); } - header('Location:index.php'); - die(); } -} \ No newline at end of file +} else { + header('HTTP/1.1 401 Unauthorized'); + echo json_encode(array( + 'code' => 401, + 'message' => 'Unauthorized')); + exit; +} diff --git a/class.php b/class.php index 87956f8..2d64b70 100644 --- a/class.php +++ b/class.php @@ -23,6 +23,11 @@ class idlersConfig const SAVE_YABS_OUTPUT = true;//true or false const GET_ASN_INFO = true;//Get ANS name and number + + //Failed attempts before ip locked from attempting login + const FAIL_ATTEMPTS_ALLOWED = 4; + //Minutes to lock ip for + const IP_LOCK_MINUTES = 10; } class elementHelpers extends idlersConfig @@ -1403,7 +1408,7 @@ class idlers extends helperFunctions $this->colOpen('col-12 col-md-6 mm-col'); $this->tagOpen('div', 'input-group'); $this->inputPrepend('Bandwidth'); - $this->numberInput('me_bandwidth', '', 'form-control', false, 1, 9999); + $this->numberInput('me_bandwidth', '', 'form-control', false, 0, 9999); $this->outputString('
TB
'); $this->tagClose('div', 3); @@ -1431,7 +1436,7 @@ class idlers extends helperFunctions $this->rowColOpen('form-row', 'col-6'); $this->tagOpen('div', 'input-group'); $this->inputPrepend('Swap'); - $this->numberInput('me_swap', '', 'form-control', false, 0.5, 9999, 'any'); + $this->numberInput('me_swap', '', 'form-control', false, 0, 9999, 'any'); $this->tagClose('div', 2); $this->colOpen('col-6'); $this->tagOpen('div', 'input-group'); @@ -1541,7 +1546,7 @@ class idlers extends helperFunctions $this->rowColOpen('form-row', 'col-12'); $this->tagOpen('div', 'input-group'); $this->inputPrepend('Bandwidth'); - $this->numberInput('sh_me_bandwidth', '', 'form-control', false, 1, 99999); + $this->numberInput('sh_me_bandwidth', '', 'form-control', false, 0, 99999); $this->outputString('
GB
'); $this->tagClose('div', 3); @@ -1928,7 +1933,7 @@ class idlers extends helperFunctions $this->colOpen('col-12 col-md-3'); $this->tagOpen('div', 'input-group'); $this->inputPrepend('Swap'); - $this->numberInput('swap', '100', 'form-control', true, 1, 62000, 'any'); + $this->numberInput('swap', '100', 'form-control', true, 0, 62000, 'any'); $this->tagClose('div', 2); $this->colOpen('col-12 col-md-3'); $this->tagOpen('div', 'input-group'); @@ -3846,4 +3851,252 @@ class itemUpdate extends idlers } } +} + +class auth extends idlers +{ + public string $token; + private string $user; + private string $ip_address; + + public function selfDestruct() + { + $select = $this->dbConnect()->prepare("SELECT `user` FROM `auth`;"); + $select->execute(); + $user_count = $select->rowCount(); + if ($user_count == 0) { + //Begin create user + $this->createAccountForm(); + } else { + //User already exists + //Delete AUTH.php + $this->deleteAuthFile(); + } + } + + protected function createAccountForm() + { + if (isset($_POST['pass']) && isset($_POST['user'])) { + //Form submitted + $this->insertAccount($_POST['user'], $_POST['pass']); + } else { + $this->pageHead(); + $this->rowColOpen('row text-center', 'col-12'); + $this->tagOpen('div', 'card'); + $this->tagOpen('div', 'card-header'); + $this->HTMLphrase('h1', '', 'Create account'); + $this->HTMLphrase('p', '', 'If you are seeing this there are currently 0 accounts. Once 1 is created this file gets deleted.'); + $this->tagClose('div'); + $this->tagOpen('div', 'card-body'); + $this->outputString('
'); + $this->rowColOpen('form-row', 'col-12 col-md-6 mm-col'); + $this->tagOpen('div', 'input-group'); + $this->inputPrepend('Username'); + $this->textInput('user', '', 'form-control', true, 4, 64); + $this->tagClose('div', 2); + $this->colOpen('col-12 col-md-6 mm-col'); + $this->tagOpen('div', 'input-group'); + $this->inputPrepend('Password'); + $this->outputString(""); + $this->tagClose('div', 3); + $this->rowColOpen('form-row text-center', 'col-12'); + $this->submitInput('Create', 'submitInput', 'btn btn-main'); + $this->tagClose('div', 2); + $this->tagClose('form'); + $this->tagClose('div', 4); + $this->pageFooter(); + } + } + + protected function insertAccount(string $user, string $pass): bool + { + $hashed_password = password_hash($pass, PASSWORD_DEFAULT);//Hash the submitted password + $insert = $this->dbConnect()->prepare("INSERT INTO `auth` (`user`, `pass`) VALUES (?,?)"); + return $insert->execute([$user, $hashed_password]); + } + + public function sessionStartIfNone() + { + if (session_status() == PHP_SESSION_NONE) { + session_start();//No session stated... so start one + } + } + + public function isLoggedIn(): bool + { + $this->sessionStartIfNone();//Start session if none already started + if (isset($_SESSION['token']) && !empty($_SESSION['token'])) { + $this->token = $_SESSION['token']; + return true;//Logged in + } else { + return false; + } + } + + public function loginForm() + { + if (isset($_POST['user']) && isset($_POST['pass'])) { + $this->attemptLogin($_POST['user'], $_POST['pass']); + } else { + $this->pageHead(); + $this->rowColOpen('row text-center', 'col-12'); + $this->tagOpen('div', 'card'); + $this->tagOpen('div', 'card-header'); + $this->HTMLphrase('h3', '', 'My idlers login'); + $this->tagClose('div'); + $this->tagOpen('div', 'card-body'); + $this->outputString(''); + $this->rowColOpen('form-row', 'col-12 col-md-3'); + $this->tagClose('div', 1); + $this->colOpen('col-12 col-md-6 mm-col'); + $this->tagOpen('div', 'input-group'); + $this->inputPrepend('Username'); + $this->textInput('user', '', 'form-control', true, 4, 64); + $this->tagClose('div', 2); + $this->colOpen('col-12 col-md-3'); + $this->tagClose('div', 2); + + $this->rowColOpen('form-row', 'col-12 col-md-3'); + $this->tagClose('div', 1); + $this->colOpen('col-12 col-md-6 mm-col'); + $this->tagOpen('div', 'input-group'); + $this->inputPrepend('Password'); + $this->outputString(""); + $this->tagClose('div', 2); + $this->colOpen('col-12 col-md-3'); + $this->tagClose('div', 2); + + $this->rowColOpen('form-row text-center', 'col-12'); + $this->submitInput('Login', 'submitInput', 'btn btn-main'); + $this->tagClose('div', 2); + $this->tagClose('form'); + $this->tagClose('div', 4); + $this->pageFooter(); + } + } + + protected function usernameExists(string $username): bool + { + $select = $this->dbConnect()->prepare("SELECT `user` FROM `auth` WHERE `user` = ? LIMIT 1;"); + $select->execute([$username]); + $row = $select->fetch(PDO::FETCH_ASSOC); + if (!empty($row)) {//Row found + $this->user = $row['user']; + return true; + } else {//NO row found + return false; + } + } + + protected function checkPasswordCorrect(string $password): bool + { + $select = $this->dbConnect()->prepare("SELECT `pass` FROM `auth` WHERE `user` = ? LIMIT 1;"); + $select->execute([$this->user]); + $row = $select->fetch(PDO::FETCH_ASSOC); + if (password_verify($password, $row['pass'])) { + return true;//Password is correct + } else { + return false;//Bad password + } + } + + protected function doLoginWasSuccess(): bool + { + $update = $this->dbConnect()->prepare("UPDATE `auth` SET `login_count` = (login_count + 1), `last_login` = NOW() WHERE `user` = ? LIMIT 1;"); + return $update->execute([$this->user]); + } + + protected function addLoginFailCount(): bool + { + $update = $this->dbConnect()->prepare("UPDATE `auth` SET login_fails = (login_fails + 1), `last_fail` = NOW() WHERE `user` = ? LIMIT 1;"); + return $update->execute([$this->user]); + } + + protected function addLoginFailAttempt(): bool + { + $insert = $this->dbConnect()->prepare('INSERT IGNORE INTO `login_attempts` (`user`, `ip`) VALUES (?, ?)'); + return $insert->execute([$this->user, $this->ip_address]); + } + + protected function setToken(int $length = 32) + { + $this->sessionStartIfNone(); + $_SESSION['token'] = $this->genID($length);//Set session as token + $this->token = $_SESSION['token']; + $update_token = $this->dbConnect()->prepare("UPDATE `auth` SET `token` = ? WHERE `user` = ? LIMIT 1;"); + $update_token->execute([$_SESSION['token'], $this->user]); + } + + protected function indexRedirect() + { + header("Location: index.php"); + exit; + } + + protected function getRecentFailCount(): int + { + $select = $this->dbConnect()->prepare("SELECT COUNT(*) as the_count FROM `login_attempts` WHERE `ip` = ? AND `datetime` > (NOW() - INTERVAL 10 MINUTE);"); + $select->execute([$this->ip_address]); + return $select->fetch()['the_count'];//login fails for IP in last 10 minutes + } + + protected function hasLockTimePassed(): bool + { + $select = $this->dbConnect()->prepare("SELECT `datetime` FROM `login_attempts` WHERE `ip` = ? ORDER BY `datetime` DESC LIMIT 1;"); + $select->execute([$this->ip_address]); + $locked_until = $select->fetch(PDO::FETCH_ASSOC); + $time = new DateTime($locked_until['datetime']); + $time->add(new DateInterval("PT" . self::IP_LOCK_MINUTES . "M")); + $locked_until_formatted = $time->format('Y-m-d H:i:s'); + $now = date('Y-m-d H:i:s'); + if ($now > $locked_until_formatted) {//Time has passed + return true; + } else { + return false; + } + } + + public function attemptLogin(string $username, string $password) + { + $this->ip_address = $_SERVER['REMOTE_ADDR']; + if ($this->getRecentFailCount() >= self::FAIL_ATTEMPTS_ALLOWED) { + if (!$this->hasLockTimePassed()) {//IP is currently not allowed to attempt login + $this->indexRedirect();//Redirect to index and show login form + } else {//IP has passed lock time + if ($this->usernameExists($username)) {//Username found + if ($this->checkPasswordCorrect($password)) {//Password is correct + $this->doLoginWasSuccess();//Add login counter and last login datetime + $this->setToken();//Set session token + $this->indexRedirect();//Redirect to index and show servers + } else {//Password is wrong + $this->addLoginFailCount(); + $this->addLoginFailAttempt(); + $this->indexRedirect();//Redirect to index and login form + } + } else {//Username not found in DB + $this->indexRedirect();//Redirect to index and show login form + } + } + } else {//A clean attempt + echo $this->getRecentFailCount(); + if ($this->usernameExists($username)) {//Username found + if ($this->checkPasswordCorrect($password)) {//Password is correct + $this->doLoginWasSuccess();//Add login counter and last login datetime + $this->setToken();//Set session token + $this->indexRedirect();//Redirect to index and show servers + } else {//Password is wrong + $this->addLoginFailCount();//Add 1 onto login fail count + $this->addLoginFailAttempt();//ip and datetime into login attempt fail logs + $this->indexRedirect();//Redirect to index and show login form + } + } else {//Username not found in DB + $this->indexRedirect();//Redirect to index and show login form + } + } + } + + protected function deleteAuthFile() + { + unlink('AUTH.php'); + } } \ No newline at end of file diff --git a/my_idlers.sql b/my_idlers.sql index d78008d..ae13528 100644 --- a/my_idlers.sql +++ b/my_idlers.sql @@ -394,6 +394,27 @@ CREATE TABLE IF NOT EXISTS `speed_tests` /*!40000 ALTER TABLE `speed_tests` ENABLE KEYS */; +CREATE TABLE IF NOT EXISTS `auth` +( + `user` varchar(64) NOT NULL, + `pass` varchar(255) DEFAULT NULL, + `token` char(32) DEFAULT NULL, + `login_count` int(11) DEFAULT 0, + `login_fails` int(11) DEFAULT 0, + `last_login` datetime DEFAULT NULL, + `last_fail` datetime DEFAULT NULL, + PRIMARY KEY (`user`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +CREATE TABLE IF NOT EXISTS `login_attempts` +( + `user` varchar(124) DEFAULT NULL, + `ip` varchar(124) DEFAULT NULL, + `datetime` datetime DEFAULT current_timestamp() +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + /*!40101 SET SQL_MODE = IFNULL(@OLD_SQL_MODE, '') */; /*!40014 SET FOREIGN_KEY_CHECKS = IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */; /*!40101 SET CHARACTER_SET_CLIENT = @OLD_CHARACTER_SET_CLIENT */; diff --git a/update1.3to1.4.sql b/update1.3to1.4.sql deleted file mode 100644 index 58ae896..0000000 --- a/update1.3to1.4.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `servers` - ADD COLUMN `asn` VARCHAR(124) NULL DEFAULT NULL AFTER `notes`; \ No newline at end of file diff --git a/update1.4to1.5.sql b/update1.4to1.5.sql new file mode 100644 index 0000000..0c1285c --- /dev/null +++ b/update1.4to1.5.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS `auth` +( + `user` varchar(64) NOT NULL, + `pass` varchar(255) DEFAULT NULL, + `token` char(32) DEFAULT NULL, + `login_count` int(11) DEFAULT 0, + `login_fails` int(11) DEFAULT 0, + `last_login` datetime DEFAULT NULL, + `last_fail` datetime DEFAULT NULL, + PRIMARY KEY (`user`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +CREATE TABLE IF NOT EXISTS `login_attempts` +( + `user` varchar(124) DEFAULT NULL, + `ip` varchar(124) DEFAULT NULL, + `datetime` datetime DEFAULT current_timestamp() +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; \ No newline at end of file