diff --git a/api.php b/api.php index 03178ea..b45877d 100644 --- a/api.php +++ b/api.php @@ -4,35 +4,4 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/** - * Simple JSON API to allow other apps to access data from this app. - * - * Requests can be sent via either GET or POST requests. POST is recommended - * as it has a lower chance of being logged on the server, exposing unencrypted - * user passwords. - */ -require __DIR__ . '/required.php'; -header("Content-Type: application/json"); - -$username = $VARS['username']; -$password = $VARS['password']; -$user = User::byUsername($username); -if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) { - header("HTTP/1.1 403 Unauthorized"); - die("\"403 Unauthorized\""); -} - -// query max results -$max = 20; -if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) { - $max = (int) $VARS['max']; -} - -switch ($VARS['action']) { - case "ping": - $out = ["status" => "OK", "maxresults" => $max, "pong" => true]; - exit(json_encode($out)); - default: - header("HTTP/1.1 400 Bad Request"); - die("\"400 Bad Request\""); -} \ No newline at end of file +require __DIR__ . "/api/index.php"; \ No newline at end of file diff --git a/api/.htaccess b/api/.htaccess new file mode 100644 index 0000000..9a4efe4 --- /dev/null +++ b/api/.htaccess @@ -0,0 +1,5 @@ +# Rewrite for Nextcloud Notes API + + RewriteEngine on + RewriteRule ([a-zA-Z0-9]+) index.php?action=$1 [PT] + \ No newline at end of file diff --git a/api/actions/ping.php b/api/actions/ping.php new file mode 100644 index 0000000..c764967 --- /dev/null +++ b/api/actions/ping.php @@ -0,0 +1,9 @@ + [ + "load" => "ping.php", + "vars" => [ + ] + ] +]; diff --git a/api/functions.php b/api/functions.php new file mode 100644 index 0000000..78e84c1 --- /dev/null +++ b/api/functions.php @@ -0,0 +1,123 @@ + 5) { + for ($i = 2; $i < strlen($key) - 2; $i++) { + $resp[$i] = "*"; + } + } + return $resp; +} + +/** + * Check if the request is allowed + * @global type $VARS + * @global type $database + * @return bool true if the request should continue, false if the request is bad + */ +function authenticate(): bool { + global $VARS, $database; + if (empty($VARS['key'])) { + return false; + } else { + $key = $VARS['key']; + if ($database->has('apikeys', ['key' => $key]) !== TRUE) { + engageRateLimit(); + http_response_code(403); + Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key); + return false; + } + } + return true; +} + +function checkVars($vars, $or = false) { + global $VARS; + $ok = []; + foreach ($vars as $key => $val) { + if (strpos($key, "OR") === 0) { + checkVars($vars[$key], true); + continue; + } + + // Only check type of optional variables if they're set, and don't + // mark them as bad if they're not set + if (strpos($key, " (optional)") !== false) { + $key = str_replace(" (optional)", "", $key); + if (empty($VARS[$key])) { + continue; + } + } else { + if (empty($VARS[$key])) { + $ok[$key] = false; + continue; + } + } + $checkmethod = "is_$val"; + if ($checkmethod($VARS[$key]) !== true) { + $ok[$key] = false; + } else { + $ok[$key] = true; + } + } + if ($or) { + $success = false; + $bad = ""; + foreach ($ok as $k => $v) { + if ($v) { + $success = true; + break; + } else { + $bad = $k; + } + } + if (!$success) { + http_response_code(400); + die("400 Bad request: variable $bad is missing or invalid"); + } + } else { + foreach ($ok as $key => $bool) { + if (!$bool) { + http_response_code(400); + die("400 Bad request: variable $key is missing or invalid"); + } + } + } +} diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..a930798 --- /dev/null +++ b/api/index.php @@ -0,0 +1,77 @@ + 1) { + $VARS["action"] = $route[0]; + } + if (count($route) >= 2 && strpos($route[1], "?") !== 0) { + $VARS["key"] = $route[1]; + + for ($i = 2; $i < count($route); $i++) { + $key = explode("=", $route[$i], 2)[0]; + $val = explode("=", $route[$i], 2)[1]; + $VARS[$key] = $val; + } + } + + if (strpos($route[count($route) - 1], "?") === 0) { + $morevars = explode("&", substr($route[count($route) - 1], 1)); + foreach ($morevars as $var) { + $key = explode("=", $var, 2)[0]; + $val = explode("=", $var, 2)[1]; + $VARS[$key] = $val; + } + } +} + +if (!authenticate()) { + http_response_code(403); + die("403 Unauthorized"); +} + +if (empty($VARS['action'])) { + http_response_code(404); + die("404 No action specified"); +} + +if (!isset($APIS[$VARS['action']])) { + http_response_code(404); + die("404 Action not defined"); +} + +$APIACTION = $APIS[$VARS["action"]]; + +if (!file_exists(__DIR__ . "/actions/" . $APIACTION["load"])) { + http_response_code(404); + die("404 Action not found"); +} + +if (!empty($APIACTION["vars"])) { + checkVars($APIACTION["vars"]); +} + +require_once __DIR__ . "/actions/" . $APIACTION["load"]; diff --git a/lib/AccountHubApi.lib.php b/lib/AccountHubApi.lib.php new file mode 100644 index 0000000..06fae3b --- /dev/null +++ b/lib/AccountHubApi.lib.php @@ -0,0 +1,54 @@ + $action, + "key" => PORTAL_KEY + ]; + if (!is_null($data)) { + $content = array_merge($content, $data); + } + $options = [ + 'http' => [ + 'method' => 'POST', + 'content' => json_encode($content), + 'header' => "Content-Type: application/json\r\n" . + "Accept: application/json\r\n", + "ignore_errors" => true + ] + ]; + + $context = stream_context_create($options); + $result = file_get_contents(PORTAL_API, false, $context); + $response = json_decode($result, true); + if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) { + if ($throwex) { + throw new Exception($result); + } else { + sendError($result); + } + } + return $response; + } + + private static function checkHttpRespCode(array $headers): bool { + foreach ($headers as $header) { + if (preg_match("/HTTP\/[0-9]\.[0-9] [0-9]{3}.*/", $header)) { + $respcode = explode(" ", $header)[1] * 1; + if ($respcode >= 200 && $respcode < 300) { + return true; + } + } + } + return false; + } + +} diff --git a/lib/Login.lib.php b/lib/Login.lib.php index fe22a38..b136c6c 100644 --- a/lib/Login.lib.php +++ b/lib/Login.lib.php @@ -74,21 +74,7 @@ class Login { */ public static function checkLoginServer() { try { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "ping" - ] - ]); - - if ($response->getStatusCode() != 200) { - return false; - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("ping"); if ($resp['status'] == "OK") { return true; } else { @@ -107,19 +93,7 @@ class Login { */ function checkAPIKey($key) { try { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => $key, - 'action' => "ping" - ] - ]); - - if ($response->getStatusCode() === 200) { - return true; - } + $resp = AccountHubApi::get("ping", null, true); return false; } catch (Exception $e) { return false; diff --git a/lib/Notifications.lib.php b/lib/Notifications.lib.php index c1d93a9..812af26 100644 --- a/lib/Notifications.lib.php +++ b/lib/Notifications.lib.php @@ -32,27 +32,15 @@ class Notifications { $timestamp = date("Y-m-d H:i:s", strtotime($timestamp)); } - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "addnotification", - 'uid' => $user->getUID(), - 'title' => $title, - 'content' => $content, - 'timestamp' => $timestamp, - 'url' => $url, - 'sensitive' => $sensitive - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("addnotification", [ + 'uid' => $user->getUID(), + 'title' => $title, + 'content' => $content, + 'timestamp' => $timestamp, + 'url' => $url, + 'sensitive' => $sensitive + ] + ); if ($resp['status'] == "OK") { return $resp['id'] * 1; } else { diff --git a/lib/User.lib.php b/lib/User.lib.php index 7852e31..752cc88 100644 --- a/lib/User.lib.php +++ b/lib/User.lib.php @@ -17,22 +17,7 @@ class User { public function __construct(int $uid, string $username = "") { // Check if user exists - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userexists", - 'uid' => $uid - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("userexists", ["uid" => $uid]); if ($resp['status'] == "OK" && $resp['exists'] === true) { $this->exists = true; } else { @@ -43,22 +28,7 @@ class User { if ($this->exists) { // Get user info - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userinfo", - 'uid' => $uid - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("userinfo", ["uid" => $uid]); if ($resp['status'] == "OK") { $this->uid = $resp['data']['uid'] * 1; $this->username = $resp['data']['username']; @@ -71,22 +41,7 @@ class User { } public static function byUsername(string $username): User { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'username' => $username, - 'action' => "userinfo" - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("userinfo", ["username" => $username]); if (!isset($resp['status'])) { sendError("Login server error: " . $resp); } @@ -105,22 +60,8 @@ class User { if (!$this->exists) { return false; } - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "hastotp", - 'username' => $this->username - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("hastotp", ['username' => $this->username]); if ($resp['status'] == "OK") { return $resp['otp'] == true; } else { @@ -150,23 +91,7 @@ class User { * @return bool */ function checkPassword(string $password): bool { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "auth", - 'username' => $this->username, - 'password' => $password - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password]); if ($resp['status'] == "OK") { return true; } else { @@ -178,23 +103,8 @@ class User { if (!$this->has2fa) { return true; } - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "verifytotp", - 'username' => $this->username, - 'code' => $code - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("verifytotp", ['username' => $this->username, 'code' => $code]); if ($resp['status'] == "OK") { return $resp['valid']; } else { @@ -209,23 +119,7 @@ class User { * @return boolean TRUE if the user has the permission (or admin access), else FALSE */ function hasPermission(string $code): bool { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "permission", - 'username' => $this->username, - 'code' => $code - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("permission", ['username' => $this->username, 'code' => $code]); if ($resp['status'] == "OK") { return $resp['has_permission']; } else { @@ -238,23 +132,7 @@ class User { * @return \AccountStatus */ function getStatus(): AccountStatus { - - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "acctstatus", - 'username' => $this->username - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("acctstatus", ['username' => $this->username]); if ($resp['status'] == "OK") { return AccountStatus::fromString($resp['account']); } else { @@ -263,23 +141,8 @@ class User { } function sendAlertEmail(string $appname = SITE_TITLE) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "alertemail", - 'username' => $this->username, - 'appname' => SITE_TITLE - ] - ]); - - if ($response->getStatusCode() > 299) { - return "An unknown error occurred."; - } + $resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => SITE_TITLE]); - $resp = json_decode($response->getBody(), TRUE); if ($resp['status'] == "OK") { return true; } else { diff --git a/mobile/index.php b/mobile/index.php index de36d52..dbb10f3 100644 --- a/mobile/index.php +++ b/mobile/index.php @@ -23,21 +23,7 @@ if ($VARS['action'] == "ping") { } function mobile_enabled() { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "mobileenabled" - ] - ]); - - if ($response->getStatusCode() > 299) { - return false; - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("mobileenabled"); if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) { return true; } else { @@ -46,26 +32,15 @@ function mobile_enabled() { } function mobile_valid($username, $code) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - "code" => $code, - "username" => $username, - 'action' => "mobilevalid" - ] - ]); + try { + $resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true); - if ($response->getStatusCode() > 299) { - return false; - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK" && $resp['valid'] === TRUE) { - return true; - } else { + if ($resp['status'] == "OK" && $resp['valid'] === TRUE) { + return true; + } else { + return false; + } + } catch (Exception $ex) { return false; } } diff --git a/settings.template.php b/settings.template.php index 2732b99..8711f01 100644 --- a/settings.template.php +++ b/settings.template.php @@ -22,7 +22,7 @@ define("SITE_TITLE", "Web App Template"); // URL of the AccountHub API endpoint -define("PORTAL_API", "http://localhost/accounthub/api.php"); +define("PORTAL_API", "http://localhost/accounthub/api/"); // URL of the AccountHub home page define("PORTAL_URL", "http://localhost/accounthub/home.php"); // AccountHub API Key