020420261226

This commit is contained in:
Manfred Aabye
2026-04-02 12:27:19 +02:00
committed by GitHub
parent 16a24007be
commit 04d3d0878e
5 changed files with 225 additions and 6 deletions
+22 -2
View File
@@ -56,20 +56,40 @@ if (($_POST['form'] ?? '') === 'login') {
$postedLang = detect_lang(supported_languages());
$_SESSION['lang'] = $postedLang;
$lang = $postedLang;
$loginIp = client_ip_address();
if (!verify_csrf($_POST['csrf'] ?? null)) {
$remainingLock = remaining_lock_seconds($config, $loginIp);
if ($remainingLock > 0) {
$minutes = (int)max(1, ceil($remainingLock / 60));
$message = str_replace('{minutes}', (string)$minutes, t($lang, 'login_locked'));
$messageType = 'error';
append_auth_audit_log($config, $loginIp, 'login_blocked', 'remaining_seconds=' . $remainingLock);
} elseif (!verify_csrf($_POST['csrf'] ?? null)) {
$message = t($lang, 'csrf_error');
$messageType = 'error';
append_auth_audit_log($config, $loginIp, 'csrf_invalid', 'login-form');
} else {
$password = (string)($_POST['password'] ?? '');
$expected = (string)($config['web_password'] ?? '');
if ($expected !== '' && hash_equals($expected, $password)) {
register_login_attempt($config, $loginIp, true);
append_auth_audit_log($config, $loginIp, 'login_success');
session_regenerate_id(true);
$_SESSION['auth'] = true;
header('Location: index.php?lang=' . urlencode($lang));
exit;
}
$message = t($lang, 'invalid_login');
register_login_attempt($config, $loginIp, false);
$remainingAfterFail = remaining_lock_seconds($config, $loginIp);
if ($remainingAfterFail > 0) {
$minutes = (int)max(1, ceil($remainingAfterFail / 60));
$message = str_replace('{minutes}', (string)$minutes, t($lang, 'login_locked'));
append_auth_audit_log($config, $loginIp, 'login_failed_locked', 'remaining_seconds=' . $remainingAfterFail);
} else {
$message = t($lang, 'invalid_login');
append_auth_audit_log($config, $loginIp, 'login_failed_password');
}
$messageType = 'error';
}
}
+15 -1
View File
@@ -8,6 +8,20 @@ return [
'runner_path' => realpath(__DIR__ . '/../bin/osmtool_web_runner.sh') ?: (__DIR__ . '/../bin/osmtool_web_runner.sh'),
'command_timeout_seconds' => 120,
'default_workdir' => '/opt',
// Optional dashboard overrides for live status cards.
'status_ports' => [
'grid-sim' => ['8002', '9010', '9020', '9030'],
'robust' => ['8002', '9010', '9020', '9030'],
'standalone' => ['9000'],
],
'status_screens' => [
'grid-sim' => ['robustserver', 'sim1', 'sim2', 'sim3'],
'robust' => ['robustserver'],
'standalone' => ['standalone'],
],
'use_sudo' => true,
'sudo_user' => 'manni',
'sudo_user' => 'sudoUserHier',
];
+11 -3
View File
@@ -8,6 +8,14 @@ return [
// Default login password. Replace immediately in production.
'web_password' => 'ChangeMeNow-2026',
// Login brute-force protection.
'auth_lockout_max_attempts' => 3,
'auth_lockout_seconds' => 1800,
// Optional custom storage paths for lockout state and audit logs.
// 'auth_store_path' => '/var/www/html/osmtool-web/var/security/login_attempts.json',
// 'auth_audit_log_path' => '/var/log/osmtool-web/auth_audit.log',
// Default assumes deployment under /var/www/html/osmtool-web.
// You can keep this dynamic path or set an absolute path manually.
'runner_path' => realpath(__DIR__ . '/../bin/osmtool_web_runner.sh') ?: (__DIR__ . '/../bin/osmtool_web_runner.sh'),
@@ -20,8 +28,8 @@ return [
// Optional dashboard overrides for live status cards.
'status_ports' => [
'grid-sim' => ['8002', '9000', '9001', '8088'],
'robust' => ['8002', '9000', '9001'],
'grid-sim' => ['8002', '9010', '9020', '9030'],
'robust' => ['8002', '9010', '9020', '9030'],
'standalone' => ['9000'],
],
@@ -36,5 +44,5 @@ return [
// If use_sudo=true, command becomes: sudo -n <sudo_user> <runner>
// Example: www-data (Linux) or your service account.
'sudo_user' => 'manni',
'sudo_user' => 'sudoUserHier',
];
+2
View File
@@ -28,6 +28,7 @@ function translations(): array
'status_ok' => 'Erfolgreich',
'status_error' => 'Fehler',
'invalid_login' => 'Anmeldung fehlgeschlagen.',
'login_locked' => 'Zu viele Fehlversuche von dieser IP. Bitte {minutes} Minute(n) warten.',
'csrf_error' => 'Ungueltiges Formular-Token.',
'hint_full' => 'Nur whitelisted OSMTool-Befehle werden ueber den Runner gestartet. Erweiterte Parameter sind modulabhaengig sichtbar.',
'action_start' => 'Start',
@@ -231,6 +232,7 @@ function translations(): array
'status_ok' => 'Success',
'status_error' => 'Error',
'invalid_login' => 'Login failed.',
'login_locked' => 'Too many failed attempts from this IP. Please wait {minutes} minute(s).',
'csrf_error' => 'Invalid form token.',
'hint_full' => 'Only whitelisted OSMTool commands are started through the runner. Advanced parameters appear only when the selected module uses them.',
'action_start' => 'Start',
+175
View File
@@ -84,3 +84,178 @@ function push_execution_history(array $entry): void
array_unshift($history, $entry);
$_SESSION['execution_history'] = array_slice($history, 0, 8);
}
function client_ip_address(): string
{
$remote = trim((string)($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
if ($remote === '') {
$remote = 'unknown';
}
$forwarded = (string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? '');
if ($forwarded !== '') {
$first = trim(explode(',', $forwarded)[0] ?? '');
if ($first !== '' && filter_var($first, FILTER_VALIDATE_IP)) {
return $first;
}
}
return $remote;
}
function security_store_path(array $config): string
{
$configured = trim((string)($config['auth_store_path'] ?? ''));
if ($configured !== '') {
return $configured;
}
return __DIR__ . '/../var/security/login_attempts.json';
}
function security_audit_log_path(array $config): string
{
$configured = trim((string)($config['auth_audit_log_path'] ?? ''));
if ($configured !== '') {
return $configured;
}
return __DIR__ . '/../var/security/auth_audit.log';
}
function ensure_parent_directory(string $filePath): void
{
$dir = dirname($filePath);
if ($dir !== '' && !is_dir($dir)) {
@mkdir($dir, 0770, true);
}
}
function auth_lockout_max_attempts(array $config): int
{
$value = (int)($config['auth_lockout_max_attempts'] ?? 3);
return $value > 0 ? $value : 3;
}
function auth_lockout_seconds(array $config): int
{
$value = (int)($config['auth_lockout_seconds'] ?? 1800);
return $value > 0 ? $value : 1800;
}
function read_auth_state(array $config): array
{
$path = security_store_path($config);
ensure_parent_directory($path);
$handle = @fopen($path, 'c+');
if (!is_resource($handle)) {
return [];
}
if (!flock($handle, LOCK_EX)) {
fclose($handle);
return [];
}
$raw = stream_get_contents($handle);
$state = [];
if (is_string($raw) && trim($raw) !== '') {
$decoded = json_decode($raw, true);
if (is_array($decoded)) {
$state = $decoded;
}
}
$now = time();
foreach ($state as $ip => $entry) {
$last = (int)($entry['last_attempt'] ?? 0);
$lockedUntil = (int)($entry['locked_until'] ?? 0);
if ($last > 0 && ($now - $last) > (7 * 24 * 3600) && $lockedUntil < $now) {
unset($state[$ip]);
}
}
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
fflush($handle);
flock($handle, LOCK_UN);
fclose($handle);
return $state;
}
function write_auth_state(array $config, array $state): void
{
$path = security_store_path($config);
ensure_parent_directory($path);
$handle = @fopen($path, 'c+');
if (!is_resource($handle)) {
return;
}
if (!flock($handle, LOCK_EX)) {
fclose($handle);
return;
}
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
fflush($handle);
flock($handle, LOCK_UN);
fclose($handle);
}
function remaining_lock_seconds(array $config, string $ip): int
{
$state = read_auth_state($config);
$entry = $state[$ip] ?? null;
if (!is_array($entry)) {
return 0;
}
$lockedUntil = (int)($entry['locked_until'] ?? 0);
$remaining = $lockedUntil - time();
return $remaining > 0 ? $remaining : 0;
}
function register_login_attempt(array $config, string $ip, bool $success): void
{
$state = read_auth_state($config);
$entry = $state[$ip] ?? ['failed_attempts' => 0, 'locked_until' => 0, 'last_attempt' => 0];
$now = time();
if ($success) {
$entry['failed_attempts'] = 0;
$entry['locked_until'] = 0;
} else {
if ((int)($entry['locked_until'] ?? 0) <= $now) {
$entry['failed_attempts'] = (int)($entry['failed_attempts'] ?? 0) + 1;
if ($entry['failed_attempts'] >= auth_lockout_max_attempts($config)) {
$entry['locked_until'] = $now + auth_lockout_seconds($config);
$entry['failed_attempts'] = 0;
}
}
}
$entry['last_attempt'] = $now;
$state[$ip] = $entry;
write_auth_state($config, $state);
}
function append_auth_audit_log(array $config, string $ip, string $event, string $detail = ''): void
{
$path = security_audit_log_path($config);
ensure_parent_directory($path);
$ua = trim((string)($_SERVER['HTTP_USER_AGENT'] ?? 'unknown-agent'));
$line = sprintf(
"%s\tip=%s\tevent=%s\tdetail=%s\tua=%s\n",
date('Y-m-d H:i:s'),
$ip,
$event,
$detail,
str_replace(["\t", "\n", "\r"], ' ', $ua)
);
@file_put_contents($path, $line, FILE_APPEND | LOCK_EX);
}