1
0

index.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. use function Hestiacp\quoteshellarg\quoteshellarg;
  3. try {
  4. require_once '../inc/vendor/autoload.php';
  5. } catch (Throwable $ex) {
  6. $errstr = 'Unable able to load required libraries. Please run v-add-sys-phpmailer in command line. Error: ' . $ex->getMessage();
  7. trigger_error($errstr);
  8. echo $errstr;
  9. exit(1);
  10. }
  11. //die("Error: Disabled");
  12. define('HESTIA_DIR_BIN', '/usr/local/hestia/bin/');
  13. define('HESTIA_CMD', '/usr/bin/sudo /usr/local/hestia/bin/');
  14. include($_SERVER['DOCUMENT_ROOT']."/inc/helpers.php");
  15. /**
  16. * Displays the error message, checks the proper code and saves a log if needed.
  17. *
  18. * @param int $exit_code
  19. * @param string $message
  20. * @param bool $add_log
  21. * @param string $user
  22. * @return void
  23. */
  24. function api_error($exit_code, $message, bool $add_log = false, $user = 'system') {
  25. $message = trim(is_array($message) ? implode("\n", $message) : $message);
  26. // Add log
  27. if ($add_log) {
  28. $v_real_user_ip = get_real_user_ip();
  29. hst_add_history_log("[$v_real_user_ip] $message", 'API', 'Error', $user);
  30. }
  31. // Print the message with http_code and exit_code
  32. $http_code = ($exit_code >= 100) ? $exit_code : exit_code_to_http_code($exit_code);
  33. header("Hestia-Exit-Code: $exit_code");
  34. http_response_code($http_code);
  35. echo (!preg_match('/^Error:/', $message)) ? "Error: $message" : $message;
  36. exit;
  37. }
  38. /**
  39. * Legacy connection format using hash or user and password.
  40. *
  41. * @param array{user: string?, pass: string?, hash?: string, cmd: string, arg1?: string, arg2?: string, arg3?: string, arg4?: string, arg5?: string, arg6?: string, arg7?: string, arg8?: string, arg9?: string, returncode?: string} $request_data
  42. * @return void
  43. * @return void
  44. */
  45. function api_legacy(array $request_data) {
  46. exec(HESTIA_CMD."v-list-sys-config json", $output, $return_var);
  47. $settings = json_decode(implode('', $output), true);
  48. unset($output);
  49. if ($settings['config']['API'] != 'yes') {
  50. echo 'Error: API has been disabled';
  51. exit;
  52. }
  53. if ($settings['config']['API_ALLOWED_IP'] != 'allow-all') {
  54. $ip_list = explode(',', $settings['config']['API_ALLOWED_IP']);
  55. $ip_list[] = '';
  56. if (!in_array(get_real_user_ip(), $ip_list)) {
  57. echo 'Error: IP is not allowed to connect with API';
  58. exit;
  59. }
  60. }
  61. //This exists, so native JSON can be used without the repeating the code twice, so future code changes are easier and don't need to be replicated twice
  62. // Authentication
  63. if (empty($request_data['hash'])) {
  64. if ($request_data['user'] != 'admin') {
  65. echo 'Error: authentication failed';
  66. exit;
  67. }
  68. $password = $request_data['password'];
  69. if (!isset($password)) {
  70. echo 'Error: missing authentication';
  71. exit;
  72. }
  73. $v_ip = quoteshellarg(get_real_user_ip());
  74. unset($output);
  75. exec(HESTIA_CMD."v-get-user-salt admin ".$v_ip." json", $output, $return_var);
  76. $pam = json_decode(implode('', $output), true);
  77. $salt = $pam['admin']['SALT'];
  78. $method = $pam['admin']['METHOD'];
  79. if ($method == 'md5') {
  80. $hash = crypt($password, '$1$'.$salt.'$');
  81. }
  82. if ($method == 'sha-512') {
  83. $hash = crypt($password, '$6$rounds=5000$'.$salt.'$');
  84. $hash = str_replace('$rounds=5000', '', $hash);
  85. }
  86. if ($method == 'yescrypt') {
  87. $fp = tmpfile();
  88. $v_password = stream_get_meta_data($fp)['uri'];
  89. fwrite($fp, $password."\n");
  90. unset($output);
  91. exec(HESTIA_CMD . 'v-check-user-password "admin" '. quoteshellarg($v_password). ' '.$v_ip.' yes', $output, $return_var);
  92. $hash = $output[0];
  93. fclose($fp);
  94. unset($output, $fp, $v_password);
  95. }
  96. if ($method == 'des') {
  97. $hash = crypt($password, $salt);
  98. }
  99. // Send hash via tmp file
  100. $v_hash = exec('mktemp -p /tmp');
  101. $fp = fopen($v_hash, "w");
  102. fwrite($fp, $hash."\n");
  103. fclose($fp);
  104. // Check user hash
  105. exec(HESTIA_CMD."v-check-user-hash admin ".$v_hash." ".$v_ip, $output, $return_var);
  106. unset($output);
  107. // Remove tmp file
  108. unlink($v_hash);
  109. // Check API answer
  110. if ($return_var > 0) {
  111. echo 'Error: authentication failed';
  112. exit;
  113. }
  114. } else {
  115. $key = '/usr/local/hestia/data/keys/'.basename($request_data['hash']);
  116. $v_ip = quoteshellarg(get_real_user_ip());
  117. exec(HESTIA_CMD."v-check-api-key ".quoteshellarg($key)." ".$v_ip, $output, $return_var);
  118. unset($output);
  119. // Check API answer
  120. if ($return_var > 0) {
  121. echo 'Error: authentication failed';
  122. exit;
  123. }
  124. }
  125. $hst_return = (($request_data['returncode'] ?? 'no') === 'yes') ? 'code' : 'data';
  126. $hst_cmd = trim($request_data['cmd'] ?? '');
  127. $hst_cmd_args = [];
  128. for ($i = 1; $i <= 9; $i++) {
  129. if (isset($request_data["arg{$i}"])) {
  130. $hst_cmd_args["arg{$i}"] = trim($request_data["arg{$i}"]);
  131. }
  132. }
  133. if (empty($hst_cmd)) {
  134. api_error(E_INVALID, "Command not provided");
  135. } else if (!preg_match('/^[a-zA-Z0-9_-]+$/', $hst_cmd)) {
  136. api_error(E_INVALID, "$hst_cmd command invalid");
  137. }
  138. // Check command
  139. if ($hst_cmd == 'v-make-tmp-file') {
  140. // Used in DNS Cluster
  141. $fp = fopen('/tmp/'.basename(escapeshellcmd($hst_cmd_args['arg2'])), 'w');
  142. fwrite($fp, $hst_cmd_args['arg1']."\n");
  143. fclose($fp);
  144. $return_var = 0;
  145. } else {
  146. // Prepare command
  147. $cmdquery = HESTIA_CMD.escapeshellcmd($hst_cmd);
  148. // Prepare arguments
  149. foreach ($hst_cmd_args as $cmd_arg) {
  150. $cmdquery .= " ".quoteshellarg($cmd_arg);
  151. }
  152. // Run cmd query
  153. exec($cmdquery, $output, $cmd_exit_code);
  154. }
  155. if ((!empty($hst_return)) && ($hst_return == 'code')) {
  156. echo $cmd_exit_code;
  157. } else {
  158. if (($return_var == 0) && (empty($output))) {
  159. echo "OK";
  160. } else {
  161. echo implode("\n", $output)."\n";
  162. }
  163. }
  164. exit;
  165. }
  166. /**
  167. * Connection using access key.
  168. *
  169. * @param array{access_key: string, secret_key: string, cmd: string, arg1?: string, arg2?: string, arg3?: string, arg4?: string, arg5?: string, arg6?: string, arg7?: string, arg8?: string, arg9?: string, returncode?: string} $request_data
  170. * @return void
  171. */
  172. function api_connection(array $request_data) {
  173. $v_real_user_ip = get_real_user_ip();
  174. exec(HESTIA_CMD."v-list-sys-config json", $output, $return_var);
  175. $settings = json_decode(implode('', $output), true);
  176. unset($output, $return_var);
  177. // Get the status of api
  178. $api_status = (!empty($settings['config']['API_SYSTEM']) && is_numeric($settings['config']['API_SYSTEM'])) ? $settings['config']['API_SYSTEM'] : 0;
  179. if ($api_status == 0) {
  180. // Check if API is disabled for all users
  181. api_error(E_DISABLED, "API has been disabled");
  182. }
  183. // Check if API access is enabled for the user
  184. if ($settings['config']['API_ALLOWED_IP'] != 'allow-all') {
  185. $ip_list = explode(',', $settings['config']['API_ALLOWED_IP']);
  186. $ip_list[] = '';
  187. if (!in_array($v_real_user_ip, $ip_list) && !in_array('0.0.0.0', $ip_list)) {
  188. api_error(E_FORBIDDEN, "IP is not allowed to connect with API");
  189. }
  190. }
  191. // Get POST Params
  192. $hst_access_key_id = trim($request_data['access_key'] ?? '');
  193. $hst_secret_access_key = trim($request_data['secret_key'] ?? '');
  194. $hst_return = (($request_data['returncode'] ?? 'no') === 'yes') ? 'code' : 'data';
  195. $hst_cmd = trim($request_data['cmd'] ?? '');
  196. $hst_cmd_args = [];
  197. for ($i = 1; $i <= 9; $i++) {
  198. if (isset($request_data["arg{$i}"])) {
  199. $hst_cmd_args["arg{$i}"] = trim($request_data["arg{$i}"]);
  200. }
  201. }
  202. if (empty($hst_cmd)) {
  203. api_error(E_INVALID, "Command not provided");
  204. } else if (!preg_match('/^[a-zA-Z0-9_-]+$/', $hst_cmd)) {
  205. api_error(E_INVALID, "$hst_cmd command invalid");
  206. }
  207. if (empty($hst_access_key_id) || empty($hst_secret_access_key)) {
  208. api_error(E_PASSWORD, "Authentication failed");
  209. }
  210. // Authenticates the key and checks permission to run the script
  211. exec(HESTIA_CMD."v-check-access-key ".quoteshellarg($hst_access_key_id)." ".quoteshellarg($hst_secret_access_key)." ".quoteshellarg($hst_cmd)." ".quoteshellarg($v_real_user_ip)." json", $output, $return_var);
  212. if ($return_var > 0) {
  213. //api_error($return_var, "Key $hst_access_key_id - authentication failed");
  214. api_error($return_var, $output);
  215. }
  216. $key_data = json_decode(implode('', $output), true) ?? [];
  217. unset($output, $return_var);
  218. $key_user = $key_data['USER'];
  219. $user_arg_position = (isset($key_data['USER_ARG_POSITION']) && is_numeric($key_data['USER_ARG_POSITION'])) ? $key_data['USER_ARG_POSITION'] : -1;
  220. # Check if API access is enabled for nonadmin users
  221. if ($key_user != 'admin' && $api_status < 2) {
  222. api_error(E_DISABLED, "API has been disabled");
  223. }
  224. // Checks if the value entered in the "user" argument matches the user of the key
  225. if ($key_user != 'admin' && $user_arg_position > 0 && $hst_cmd_args["arg{$user_arg_position}"] != $key_user) {
  226. api_error(E_FORBIDDEN, "Key $hst_access_key_id - the \"user\" argument doesn\'t match the key\'s user");
  227. }
  228. // Prepare command
  229. $cmdquery = HESTIA_CMD.escapeshellcmd($hst_cmd);
  230. // Prepare arguments
  231. foreach ($hst_cmd_args as $cmd_arg) {
  232. $cmdquery .= " ".quoteshellarg($cmd_arg);
  233. }
  234. # v-make-temp files is manodory other wise some functions will break
  235. if ($hst_cmd == 'v-make-tmp-file'){
  236. $fp = fopen('/tmp/'.basename($hst_cmd_args['arg2']), 'w');
  237. fwrite($fp, $hst_cmd_args['arg1']."\n");
  238. fclose($fp);
  239. $cmd_exit_code = 0;
  240. }else{
  241. // Run cmd query
  242. exec($cmdquery, $output, $cmd_exit_code);
  243. $cmd_output = trim(implode("\n", $output));
  244. unset($output);
  245. }
  246. header("Hestia-Exit-Code: $cmd_exit_code");
  247. if ($hst_return == 'code') {
  248. echo $cmd_exit_code;
  249. } else {
  250. if ($cmd_exit_code > 0) {
  251. http_response_code(exit_code_to_http_code($cmd_exit_code));
  252. } else {
  253. http_response_code(!empty($cmd_output) ? 200 : 204);
  254. if (!empty($cmd_output) && json_decode($cmd_output, true)) {
  255. header('Content-Type: application/json; charset=utf-8');
  256. }
  257. }
  258. echo $cmd_output;
  259. }
  260. exit;
  261. }
  262. // Get request data
  263. if (isset($_POST['access_key']) || isset($_POST['user']) || isset($_POST['hash'])) {
  264. $request_data = $_POST;
  265. } else if (($json_data = json_decode(file_get_contents("php://input"), true)) != null) {
  266. $request_data = $json_data;
  267. } else {
  268. api_error(405, "Error: data received is null or invalid, check https://docs.hestiacp.com/admin_docs/api.html");
  269. }
  270. // Try to get access key in the hash
  271. if (!isset($request_data['access_key']) && isset($request_data['hash']) && substr_count($request_data['hash'], ':') == 1) {
  272. $hash_parts = explode(':', $request_data['hash']);
  273. if (strlen($hash_parts[0]) == 20 && strlen($hash_parts[1]) == 40) {
  274. $request_data['access_key'] = $hash_parts[0];
  275. $request_data['secret_key'] = $hash_parts[1];
  276. unset($request_data['hash']);
  277. }
  278. }
  279. // Check data format
  280. if (isset($request_data['access_key']) && isset($request_data['secret_key'])) {
  281. api_connection($request_data);
  282. } else if (isset($request_data['user']) || isset($request_data['hash'])) {
  283. api_legacy($request_data);
  284. } else {
  285. api_error(405, "Error: data received is null or invalid, check https://docs.hestiacp.com/admin_docs/api.html");
  286. }