index.php 12 KB

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