= ~PHP_INT_MAX); } function cleanupFails(string $failFile, int $time): int { global $cooldown; // Read existing fails @mkdir(dirname($failFile), 0755); $failText = readFromFile($failFile); $fails = explode("\n", trim($failText)); // Remove entries older than $cooldown minutes, and entries that are not timestamps $updateFails = false; foreach ((array) $fails as $key => $value) { if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) { unset ($fails[$key]); $updateFails = true; } } // Save fails to disk if ($updateFails) { $failText = implode("\n", $fails)."\n"; writeToFile($failFile, $failText); } return count($fails); } function verifyUsernamePassword(string $username, string $password): bool { if ($username != "root") return false; // @TODO: integrate with PAM to avoid direct access to /etc/shadow and validate other user names $output = exec("/usr/bin/grep root /etc/shadow"); if ($output === false) return false; $credentials = explode(":", $output); return password_verify($password, $credentials[1]); } function verifyTwoFactorToken(string $username, string $token): bool { try { // Create curl client $curlClient = curl_init(); curl_setopt($curlClient, CURLOPT_HEADER, true); curl_setopt($curlClient, CURLOPT_RETURNTRANSFER, true); curl_setopt($curlClient, CURLOPT_UNIX_SOCKET_PATH, '/var/run/unraid-api.sock'); curl_setopt($curlClient, CURLOPT_URL, 'http://unixsocket/verify'); curl_setopt($curlClient, CURLOPT_BUFFERSIZE, 256); curl_setopt($curlClient, CURLOPT_TIMEOUT, 5); curl_setopt($curlClient, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Origin: /var/run/unraid-notifications.sock')); curl_setopt($curlClient, CURLOPT_POSTFIELDS, json_encode([ 'username' => $username, 'token' => $token ])); // Send the request curl_exec($curlClient); // Get the http status code $httpCode = curl_getinfo($curlClient, CURLINFO_HTTP_CODE); // Close the connection curl_close($curlClient); // Error // This should accept 200 or 204 status codes if ($httpCode !== 200 && $httpCode !== 204) { // Log error to syslog exec("logger -t webGUI ".escapeshellarg("2FA code for {$username} is invalid, blocking access!")); return false; } // Log success to syslog exec("logger -t webGUI ".escapeshellarg("2FA code for {$username} is valid, allowing login!")); // Success return true; } catch (Exception $exception) { // Error return false; } } // Check if a haystack ends in a needle function endsWith($haystack, $needle): bool { return substr_compare($haystack, $needle, -strlen($needle)) === 0; } // Check if we're accessing this via a wildcard cert function isWildcardCert(): bool { global $server_name; return endsWith($server_name, '.myunraid.net'); } // Check if we're accessing this locally via the expected myunraid.net url function isLocalAccess(): bool { global $nginx, $server_name; return isWildcardCert() && $nginx['NGINX_LANFQDN'] === $server_name; } // Check if we're accessing this remotely via the expected myunraid.net url function isRemoteAccess(): bool { global $nginx, $server_name; return isWildcardCert() && $nginx['NGINX_WANFQDN'] === $server_name; } // Check if 2fa is enabled for local (requires USE_SSL to be "auto" so no alternate urls can access the server) function isLocalTwoFactorEnabled(): bool { global $nginx, $my_servers; return $nginx['NGINX_USESSL'] === "auto" && $my_servers['local']['2Fa'] === 'yes'; } // Check if 2fa is enabled for remote function isRemoteTwoFactorEnabled(): bool { global $my_servers; return $my_servers['remote']['2Fa'] === 'yes'; } // Load configs into memory $my_servers = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true); $nginx = @parse_ini_file('/var/local/emhttp/nginx.ini'); // Vars $maxFails = 3; $cooldown = 15 * 60; // 15 mins $remote_addr = $_SERVER['REMOTE_ADDR'] ?? "unknown"; $failFile = "/var/log/pwfail/{$remote_addr}"; // Get the credentials $username = $_POST['username']; $password = $_POST['password']; $token = $_REQUEST['token']; // Check if we need 2fa $twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled()); // If we have a username + password combo attempt to login if (!empty($username) && !empty($password)) { try { // Bail if we're missing the 2FA token and we expect one if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected')); // Read existing fails, cleanup expired ones $time = time(); $failCount = cleanupFails($failFile, $time); // Check if we're limited if ($failCount >= $maxFails) { if ($failCount == $maxFails) exec("logger -t webGUI ".escapeshellarg("Ignoring login attempts for {$username} from {$remote_addr}")); throw new Exception(_('Too many invalid login attempts')); } // Bail if username + password combo doesn't work if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password')); // Bail if we need a token but it's invalid if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token')); // Successful login, start session @unlink($failFile); session_start(); $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = $username; session_regenerate_id(true); session_write_close(); exec("logger -t webGUI ".escapeshellarg("Successful login user {$username} from {$remote_addr}")); // Redirect the user to the start page header("Location: /".$var['START_PAGE']); exit; } catch (Exception $exception) { // Set error message $error = $exception->getMessage(); // Log error to syslog exec("logger -t webGUI ".escapeshellarg("Unsuccessful login user {$username} from {$remote_addr}")); appendToFile($failFile, $time."\n"); } } $boot = "/boot/config/plugins/dynamix"; $myFile = "case-model.cfg"; $myCase = file_exists("$boot/$myFile") ? file_get_contents("$boot/$myFile") : false; extract(parse_plugin_cfg('dynamix', true)); $theme_dark = in_array($display['theme'], ['black', 'gray']); ?>