diff --git a/api.php b/api.php new file mode 100644 index 00000000..42bfaae4 --- /dev/null +++ b/api.php @@ -0,0 +1,69 @@ +$method(); +} catch (Exception $e) { + json_response($e->getCode(), $e->getMessage()); +} + +exit(); + +/** + * output json result + * + * @param int $status + * @param string $status_message + * @param mixed $data + * + * @return void + */ +function json_response($status, $status_message, $data = null) +{ + header("HTTP/1.1 " . $status); + + $response['status'] = $status; + $response['status_message'] = $status_message; + $response['data'] = $data; + + $json_response = json_encode($response, JSON_PRETTY_PRINT); + echo $json_response; + exit(); +} diff --git a/install/froxlor.sql b/install/froxlor.sql index 870e30d4..5a262c55 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -1042,3 +1042,18 @@ CREATE TABLE `panel_plans` ( KEY adminid (adminid) ) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; + +DROP TABLE IF EXISTS `api_keys`; +CREATE TABLE `api_keys` ( + `id` int(11) NOT NULL auto_increment, + `adminid` int(11) NOT NULL default '0', + `customerid` int(11) NOT NULL default '0', + `apikey` varchar(500) NOT NULL default '', + `secret` varchar(500) NOT NULL default '', + `allowed_from` text NOT NULL, + `valid_until` int(15) NOT NULL default '0', + PRIMARY KEY (id), + KEY adminid (adminid), + KEY customerid (customerid) +) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; + diff --git a/lib/classes/api/abstract.ApiCommand.php b/lib/classes/api/abstract.ApiCommand.php new file mode 100644 index 00000000..43a5b688 --- /dev/null +++ b/lib/classes/api/abstract.ApiCommand.php @@ -0,0 +1,129 @@ +cmd_params = $params; + if (! empty($header)) { + $this->readUserData($header); + } elseif (! empty($userinfo)) { + $this->user_data = $userinfo; + $this->is_admin = ($userinfo['adminsession'] == 1 && $userinfo['adminid'] > 0) ? true : false; + } else { + throw new Exception("Invalid user data", 500); + } + $this->logger = FroxlorLogger::getInstanceOf($this->user_data); + } + + public static function getLocal($userinfo = null, $params = null) + { + return new static(null, $params, $userinfo); + } + + /** + * admin flag + * + * @return boolean + */ + protected function isAdmin() + { + return $this->is_admin; + } + + /** + * return field from user-table + * + * @param string $detail + * + * @return string + */ + protected function getUserDetail($detail = null) + { + return (isset($this->user_data[$detail]) ? $this->user_data[$detail] : null); + } + + /** + * receive field from parameter-list + * + * @param string $param + * + * @throws Exception + * @return mixed + */ + protected function getParam($param = null) + { + if (isset($this->cmd_params[$param])) { + return $this->cmd_params[$param]; + } + return null; + } + + /** + * return logger instance + * + * @return FroxlorLogger + */ + protected function logger() + { + return $this->logger; + } + + protected function response($status, $status_message, $data = null) + { + header("HTTP/1.1 " . $status); + + $response['status'] = $status; + $response['status_message'] = $status_message; + $response['data'] = $data; + + $json_response = json_encode($response, JSON_PRETTY_PRINT); + return $json_response; + } + + public abstract function list(); + + public abstract function get(); + + public abstract function add(); + + public abstract function update(); + + public abstract function delete(); + + private function readUserData($header = null) + { + $sel_stmt = Database::prepare("SELECT * FROM `api_keys` WHERE `apikey` = :ak AND `secret` = :as"); + $result = Database::pexecute_first($sel_stmt, array( + 'ak' => $header['apikey'], + 'as' => $header['secret'] + ), true, true); + if ($result) { + // admin or customer? + if ($result['customerid'] == 0) { + $this->is_admin = true; + $table = 'panel_admins'; + $key = "adminid"; + } else { + $this->is_admin = false; + $table = 'panel_customers'; + $key = "customerid"; + } + $sel_stmt = Database::prepare("SELECT * FROM `" . $table . "` WHERE `" . $key . "` = :id"); + $this->user_data = Database::pexecute_first($sel_stmt, array( + 'id' => ($this->is_admin ? $result['adminid'] : $result['customerid']) + ), true, true); + return true; + } + throw new Exception("Invalid API credentials", 400); + } +} diff --git a/lib/classes/api/api_includes.inc.php b/lib/classes/api/api_includes.inc.php new file mode 100644 index 00000000..09b3cd84 --- /dev/null +++ b/lib/classes/api/api_includes.inc.php @@ -0,0 +1,6 @@ + $key, + 'as' => $secret + ), true, true); + if ($result) { + if ($result['apikey'] == $key && $result['secret'] == $secret && ($result['valid_until'] == -1 || $result['valid_until'] >= time())) { + if (!empty($result['allowed_from'])) { + $ip_list = explode(",", $result['allowed_from']); + $access_ip = $_SERVER['REMOTE_ADDR']; + // @fixme finish me + } + return true; + } + } + throw new Exception("Invalid authorization credentials", 400); + } + + /** + * validates the given command + * + * @param array $body + * + * @throws Exception + * @return boolean + */ + private static function validateBody($request) + { + // check body + if (! isset($request['body']) || empty($request['body'])) { + throw new Exception("Invalid request body", 400); + } + + // check command exists + if (! isset($request['body']['command']) || empty($request['body']['command'])) { + throw new Exception("No command given", 400); + } + + $command = explode(".", $request['body']['command']); + + if (count($command) != 2) { + throw new Exception("Invalid command", 400); + } + // simply check for file-existance, as we do not want to use our autoloader because this way + // it will recognize non-api classes+methods as valid commands + $apiclass = FROXLOR_INSTALL_DIR . '/lib/classes/api/commands/class.' . $command[0] . '.php'; + if (! file_exists($apiclass) || ! @method_exists($command[0], $command[1])) { + // there will be an exception from the autoloader for class_exists hence the try-catch-block + throw new Exception("Unknown command", 400); + } + return array( + 'command' => array( + 'class' => $command[0], + 'method' => $command[1] + ), + 'params' => isset($request['body']['params']) ? $request['body']['params'] : null + ); + } +} diff --git a/lib/classes/database/class.Database.php b/lib/classes/database/class.Database.php index 3174c2b3..7f6c7d46 100644 --- a/lib/classes/database/class.Database.php +++ b/lib/classes/database/class.Database.php @@ -67,11 +67,11 @@ class Database { * @param array $params (optional) * @param bool $showerror suppress errordisplay (default true) */ - public static function pexecute(&$stmt, $params = null, $showerror = true) { + public static function pexecute(&$stmt, $params = null, $showerror = true, $json_response = false) { try { $stmt->execute($params); } catch (PDOException $e) { - self::_showerror($e, $showerror); + self::_showerror($e, $showerror, $json_response); } } @@ -86,8 +86,8 @@ class Database { * * @return array */ - public static function pexecute_first(&$stmt, $params = null, $showerror = true) { - self::pexecute($stmt, $params, $showerror); + public static function pexecute_first(&$stmt, $params = null, $showerror = true, $json_response = false) { + self::pexecute($stmt, $params, $showerror, $json_response); return $stmt->fetch(PDO::FETCH_ASSOC); } @@ -309,7 +309,7 @@ class Database { * @param PDOException $error * @param bool $showerror if set to false, the error will be logged but we go on */ - private static function _showerror($error, $showerror = true) { + private static function _showerror($error, $showerror = true, $json_response = false) { global $userinfo, $theme, $linker; // include userdata.inc.php @@ -367,6 +367,10 @@ class Database { @fwrite($errlog, "|TRACE\n".$error_trace."\n"); @fclose($errlog); + if ($showerror && $json_response) { + throw new Exception($error_message, 500); + } + if ($showerror) { if (empty($sql['debug'])) { $error_trace = ''; diff --git a/lib/functions/output/function.standard_error.php b/lib/functions/output/function.standard_error.php index 03fb272f..9e8bd7fb 100644 --- a/lib/functions/output/function.standard_error.php +++ b/lib/functions/output/function.standard_error.php @@ -25,7 +25,7 @@ * @author Florian Lippert * @author Ron Brand */ -function standard_error($errors = '', $replacer = '') { +function standard_error($errors = '', $replacer = '', $throw_exception = false) { global $userinfo, $s, $header, $footer, $lng, $theme; @@ -60,6 +60,9 @@ function standard_error($errors = '', $replacer = '') { } } + if ($throw_exception) { + throw new Exception($error); + } eval("echo \"" . getTemplate('misc/error', '1') . "\";"); exit; } diff --git a/lib/functions/validate/function.validate.php b/lib/functions/validate/function.validate.php index eddb8c3c..355f48e1 100644 --- a/lib/functions/validate/function.validate.php +++ b/lib/functions/validate/function.validate.php @@ -31,7 +31,7 @@ * */ -function validate($str, $fieldname, $pattern = '', $lng = '', $emptydefault = array()) { +function validate($str, $fieldname, $pattern = '', $lng = '', $emptydefault = array(), $throw_exception = false) { global $log, $theme; @@ -74,6 +74,6 @@ function validate($str, $fieldname, $pattern = '', $lng = '', $emptydefault = ar $lng = 'stringformaterror'; } - standard_error($lng, $fieldname); + standard_error($lng, $fieldname, $throw_exception); exit; } diff --git a/lib/functions/validate/function.validate_ip.php b/lib/functions/validate/function.validate_ip.php index f3caa492..2a30150b 100644 --- a/lib/functions/validate/function.validate_ip.php +++ b/lib/functions/validate/function.validate_ip.php @@ -23,7 +23,7 @@ * @return mixed ip address on success, standard_error on failure * @deprecated use validate_ip2 */ -function validate_ip($ip, $return_bool = false, $lng = 'invalidip') { +function validate_ip($ip, $return_bool = false, $lng = 'invalidip', $throw_exception = false) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false @@ -32,7 +32,7 @@ function validate_ip($ip, $return_bool = false, $lng = 'invalidip') { if ($return_bool) { return false; } else { - standard_error($lng, $ip); + standard_error($lng, $ip, $throw_exception); exit(); } } else { @@ -53,7 +53,7 @@ function validate_ip($ip, $return_bool = false, $lng = 'invalidip') { * * @return string|bool ip address on success, false on failure */ -function validate_ip2($ip, $return_bool = false, $lng = 'invalidip', $allow_localhost = false, $allow_priv = false, $allow_cidr = false) { +function validate_ip2($ip, $return_bool = false, $lng = 'invalidip', $allow_localhost = false, $allow_priv = false, $allow_cidr = false, $throw_exception = false) { $cidr = ""; if ($allow_cidr) { @@ -69,7 +69,7 @@ function validate_ip2($ip, $return_bool = false, $lng = 'invalidip', $allow_loca if ($return_bool) { return false; } else { - standard_error($lng, $ip); + standard_error($lng, $ip, $throw_exception); exit(); } } @@ -91,7 +91,7 @@ function validate_ip2($ip, $return_bool = false, $lng = 'invalidip', $allow_loca if ($return_bool) { return false; } else { - standard_error($lng, $ip); + standard_error($lng, $ip, $throw_exception); exit(); } }