010420262055 first

This commit is contained in:
Manfred Aabye
2026-04-01 20:55:45 +02:00
committed by GitHub
parent ab20d9de73
commit b9113793d2
30 changed files with 13205 additions and 1 deletions
+77
View File
@@ -0,0 +1,77 @@
# OSMTool Web PHP (DE/EN/FR/ES)
This folder contains a minimal and secure PHP web controller for `osmtool_main.sh`.
## Features
- Login-protected web UI
- Language switch: `de`, `en`, `fr`, `es`
- CSRF protection
- Whitelisted actions only
- Runner script wrapper for safer command execution
## File Layout
- `public/index.php` - web UI and request handling
- `src/bootstrap.php` - session, auth helpers, CSRF
- `src/i18n.php` - translations
- `src/runner.php` - command whitelist and execution
- `src/config.sample.php` - configuration template
- `bin/osmtool_web_runner.sh` - controlled bridge to `osmtool_main.sh`
## Setup
1. Typical Linux deploy path:
```bash
sudo mkdir -p /var/www/html/osmtool-web
sudo cp -a webphp/. /var/www/html/osmtool-web/
sudo chown -R www-data:www-data /var/www/html/osmtool-web
```
1. Edit config values:
```bash
nano /var/www/html/osmtool-web/src/config.php
```
1. Set execute bit for runner and main script:
```bash
chmod +x /var/www/html/osmtool-web/bin/osmtool_web_runner.sh
chmod +x /opt/opensimMULTITOOLS-II/osmtool_main.sh
```
1. Configure web server document root to:
```text
/var/www/html/osmtool-web/public
```
1. Open in browser and log in with `web_password` from config.
## Sudoers Example (recommended)
Use `visudo` and allow only the web runner:
```text
www-data ALL=(manni) NOPASSWD: /var/www/html/osmtool-web/bin/osmtool_web_runner.sh
```
Then keep `use_sudo=true` and `sudo_user=manni` in config.
## Supported Actions
- `start`
- `stop`
- `restart`
- `smoke`
- `report`
- `health`
- `cron-list`
## Notes
- Do not expose this UI publicly without HTTPS and IP restrictions.
- Keep the password strong and unique.
- Extend actions only in `src/runner.php` and `bin/osmtool_web_runner.sh` together.
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MAIN_SCRIPT="$PROJECT_ROOT/osmtool_main.sh"
if [[ ! -x "$MAIN_SCRIPT" ]]; then
echo "ERROR: osmtool_main.sh not executable: $MAIN_SCRIPT" >&2
exit 1
fi
ACTION="${1:-}"
PROFILE="${2:-grid-sim}"
LANG="${3:-de}"
WORKDIR="${4:-/opt}"
case "$PROFILE" in
grid-sim|robust|standalone) ;;
*) echo "ERROR: invalid profile" >&2; exit 2 ;;
esac
case "$LANG" in
de|en|fr|es) ;;
*) echo "ERROR: invalid language" >&2; exit 2 ;;
esac
run_main() {
bash "$MAIN_SCRIPT" --mode cli --profile "$PROFILE" --lang "$LANG" --workdir "$WORKDIR" "$@"
}
case "$ACTION" in
start)
run_main --module startstop --action start
;;
stop)
run_main --module startstop --action stop
;;
restart)
run_main --module startstop --action restart
;;
smoke)
run_main --module smoke --action run
;;
report)
run_main --module report --action generate
;;
health)
run_main --module health --action run
;;
cron-list)
run_main --module cron --action list
;;
*)
echo "ERROR: unsupported action" >&2
exit 2
;;
esac
+264
View File
@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/bootstrap.php';
require_once __DIR__ . '/../src/runner.php';
$lang = detect_lang(supported_languages());
$_SESSION['lang'] = $lang;
$message = '';
$messageType = 'ok';
$resultOutput = '';
$resultCode = null;
if (isset($_GET['logout'])) {
$_SESSION = [];
session_destroy();
header('Location: index.php?lang=' . urlencode($lang));
exit;
}
if (($_POST['form'] ?? '') === 'login') {
$postedLang = detect_lang(supported_languages());
$_SESSION['lang'] = $postedLang;
$lang = $postedLang;
if (!verify_csrf($_POST['csrf'] ?? null)) {
$message = t($lang, 'csrf_error');
$messageType = 'error';
} else {
$password = (string)($_POST['password'] ?? '');
$expected = (string)($config['web_password'] ?? '');
if ($expected !== '' && hash_equals($expected, $password)) {
$_SESSION['auth'] = true;
header('Location: index.php?lang=' . urlencode($lang));
exit;
}
$message = t($lang, 'invalid_login');
$messageType = 'error';
}
}
if (is_logged_in() && (($_POST['form'] ?? '') === 'run')) {
if (!verify_csrf($_POST['csrf'] ?? null)) {
$message = t($lang, 'csrf_error');
$messageType = 'error';
} else {
$action = (string)($_POST['action'] ?? '');
$profile = (string)($_POST['profile'] ?? 'grid-sim');
$workdir = (string)($_POST['workdir'] ?? (string)($config['default_workdir'] ?? '/opt'));
$result = run_whitelisted_action($config, $action, $profile, $lang, $workdir);
$resultOutput = (string)($result['output'] ?? '');
$resultCode = (int)($result['exitCode'] ?? 1);
if (($result['ok'] ?? false) === true) {
$message = t($lang, 'status_ok') . ' (exit=' . $resultCode . ')';
$messageType = 'ok';
} else {
$message = t($lang, 'status_error') . ' (exit=' . $resultCode . ')';
$messageType = 'error';
}
}
}
$actions = [
'start' => t($lang, 'action_start'),
'stop' => t($lang, 'action_stop'),
'restart' => t($lang, 'action_restart'),
'smoke' => t($lang, 'action_smoke'),
'report' => t($lang, 'action_report'),
'health' => t($lang, 'action_health'),
'cron-list' => t($lang, 'action_cron_list'),
];
$profiles = [
'grid-sim' => 'grid-sim',
'robust' => 'robust',
'standalone' => 'standalone',
];
?>
<!doctype html>
<html lang="<?= h($lang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h(t($lang, 'title')) ?></title>
<style>
:root {
--bg1: #0f172a;
--bg2: #1e293b;
--card: #111827;
--line: #334155;
--text: #e5e7eb;
--muted: #94a3b8;
--ok: #16a34a;
--err: #dc2626;
--btn: #0284c7;
--btnHover: #0369a1;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", Tahoma, sans-serif;
background: radial-gradient(1000px 600px at 20% 0%, #1d4ed8 0%, transparent 60%),
radial-gradient(900px 500px at 100% 100%, #0ea5e9 0%, transparent 50%),
linear-gradient(160deg, var(--bg1), var(--bg2));
color: var(--text);
min-height: 100vh;
padding: 24px;
}
.wrap { max-width: 980px; margin: 0 auto; }
.card {
background: color-mix(in oklab, var(--card), black 8%);
border: 1px solid var(--line);
border-radius: 14px;
padding: 18px;
box-shadow: 0 12px 40px rgba(0,0,0,.25);
margin-bottom: 18px;
}
h1 { margin: 0 0 8px; font-size: 1.7rem; }
p { margin: 0 0 12px; color: var(--muted); }
.topbar {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 14px;
}
label { display: block; margin-bottom: 6px; font-weight: 600; }
input, select, button {
width: 100%;
border-radius: 10px;
border: 1px solid var(--line);
background: #0b1220;
color: var(--text);
padding: 10px 12px;
}
button {
background: var(--btn);
border: none;
font-weight: 700;
cursor: pointer;
}
button:hover { background: var(--btnHover); }
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.full { grid-column: 1 / -1; }
.msg {
margin: 10px 0;
padding: 10px 12px;
border-radius: 10px;
font-weight: 700;
}
.ok { background: rgba(22,163,74,.2); border: 1px solid rgba(22,163,74,.5); }
.error { background: rgba(220,38,38,.2); border: 1px solid rgba(220,38,38,.5); }
pre {
background: #020617;
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px;
overflow: auto;
max-height: 420px;
margin: 0;
white-space: pre-wrap;
}
.lang-links a {
color: #bae6fd;
margin-right: 8px;
text-decoration: none;
}
.lang-links a:hover { text-decoration: underline; }
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="topbar">
<div>
<h1><?= h(t($lang, 'title')) ?></h1>
<p><?= h(t($lang, 'subtitle')) ?></p>
</div>
<div class="lang-links">
<a href="?lang=de">DE</a>
<a href="?lang=en">EN</a>
<a href="?lang=fr">FR</a>
<a href="?lang=es">ES</a>
<?php if (is_logged_in()): ?>
| <a href="?logout=1&amp;lang=<?= h($lang) ?>"><?= h(t($lang, 'logout')) ?></a>
<?php endif; ?>
</div>
</div>
<?php if ($message !== ''): ?>
<div class="msg <?= h($messageType) ?>"><?= h($message) ?></div>
<?php endif; ?>
<?php if (!is_logged_in()): ?>
<form method="post">
<input type="hidden" name="form" value="login">
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="lang" value="<?= h($lang) ?>">
<div class="grid">
<div class="full">
<label><?= h(t($lang, 'password')) ?></label>
<input type="password" name="password" autocomplete="current-password" required>
</div>
<div class="full">
<button type="submit"><?= h(t($lang, 'login')) ?></button>
</div>
</div>
</form>
<?php else: ?>
<p><?= h(t($lang, 'hint')) ?></p>
<form method="post">
<input type="hidden" name="form" value="run">
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="lang" value="<?= h($lang) ?>">
<div class="grid">
<div>
<label><?= h(t($lang, 'profile')) ?></label>
<select name="profile" required>
<?php foreach ($profiles as $value => $label): ?>
<option value="<?= h($value) ?>"><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label><?= h(t($lang, 'action')) ?></label>
<select name="action" required>
<?php foreach ($actions as $value => $label): ?>
<option value="<?= h($value) ?>"><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="full">
<label><?= h(t($lang, 'workdir')) ?></label>
<input name="workdir" value="<?= h((string)($config['default_workdir'] ?? '/opt')) ?>">
</div>
<div class="full">
<button type="submit"><?= h(t($lang, 'execute')) ?></button>
</div>
</div>
</form>
<?php if ($resultCode !== null): ?>
<div class="card" style="margin-top:14px;">
<label><?= h(t($lang, 'output')) ?></label>
<pre><?= h($resultOutput) ?></pre>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
$configFile = __DIR__ . '/config.php';
if (!is_file($configFile)) {
$configFile = __DIR__ . '/config.sample.php';
}
$config = require $configFile;
if (!isset($config['session_key']) || !is_string($config['session_key'])) {
throw new RuntimeException('Missing session_key in config.');
}
session_name('osmtool_web');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => false,
'httponly' => true,
'samesite' => 'Strict',
]);
session_start();
if (!isset($_SESSION['session_seed'])) {
$_SESSION['session_seed'] = hash('sha256', $config['session_key'] . random_int(1000, 999999));
}
require_once __DIR__ . '/i18n.php';
function is_logged_in(): bool
{
return isset($_SESSION['auth']) && $_SESSION['auth'] === true;
}
function require_login(): void
{
if (!is_logged_in()) {
header('Location: index.php');
exit;
}
}
function csrf_token(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function verify_csrf(?string $token): bool
{
if (!isset($_SESSION['csrf_token']) || !is_string($token)) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
+13
View File
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
return [
'session_key' => 'change-me-super-random-session-key',
'web_password' => 'ChangeMeNow-2026',
// Default assumes deployment under /var/www/html/osmtool-web
'runner_path' => realpath(__DIR__ . '/../bin/osmtool_web_runner.sh') ?: (__DIR__ . '/../bin/osmtool_web_runner.sh'),
'command_timeout_seconds' => 120,
'default_workdir' => '/opt',
'use_sudo' => true,
'sudo_user' => 'manni',
];
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
return [
// Set this to a strong random value in production.
'session_key' => 'change-me-super-random-session-key',
// Default login password. Replace immediately in production.
'web_password' => 'ChangeMeNow-2026',
// 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'),
// Web timeout in seconds.
'command_timeout_seconds' => 120,
// Default workdir passed to osmtool_main.sh.
'default_workdir' => '/opt',
// Allow running through sudo if needed.
'use_sudo' => true,
// If use_sudo=true, command becomes: sudo -n <sudo_user> <runner>
// Example: www-data (Linux) or your service account.
'sudo_user' => 'manni',
];
+140
View File
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
function supported_languages(): array
{
return ['de', 'en', 'fr', 'es'];
}
function translations(): array
{
return [
'de' => [
'title' => 'OSMTool Web Steuerung',
'subtitle' => 'Sichere Web-Oberflaeche fuer OpenSim Aktionen',
'login_title' => 'Anmeldung',
'password' => 'Passwort',
'login' => 'Einloggen',
'logout' => 'Ausloggen',
'language' => 'Sprache',
'profile' => 'Profil',
'action' => 'Aktion',
'execute' => 'Ausfuehren',
'workdir' => 'Workdir',
'output' => 'Ausgabe',
'status_ok' => 'Erfolgreich',
'status_error' => 'Fehler',
'invalid_login' => 'Anmeldung fehlgeschlagen.',
'csrf_error' => 'Ungueltiges Formular-Token.',
'not_allowed' => 'Aktion nicht erlaubt.',
'run_failed' => 'Ausfuehrung fehlgeschlagen.',
'hint' => 'Es werden nur freigegebene Aktionen ausgefuehrt.',
'action_start' => 'Start',
'action_stop' => 'Stop',
'action_restart' => 'Neustart',
'action_smoke' => 'Smoke-Test',
'action_report' => 'Report erzeugen',
'action_health' => 'Healthcheck',
'action_cron_list' => 'Cronjobs anzeigen',
],
'en' => [
'title' => 'OSMTool Web Control',
'subtitle' => 'Secure web interface for OpenSim actions',
'login_title' => 'Login',
'password' => 'Password',
'login' => 'Sign in',
'logout' => 'Sign out',
'language' => 'Language',
'profile' => 'Profile',
'action' => 'Action',
'execute' => 'Execute',
'workdir' => 'Workdir',
'output' => 'Output',
'status_ok' => 'Success',
'status_error' => 'Error',
'invalid_login' => 'Login failed.',
'csrf_error' => 'Invalid form token.',
'not_allowed' => 'Action not allowed.',
'run_failed' => 'Execution failed.',
'hint' => 'Only whitelisted actions are executed.',
'action_start' => 'Start',
'action_stop' => 'Stop',
'action_restart' => 'Restart',
'action_smoke' => 'Smoke test',
'action_report' => 'Generate report',
'action_health' => 'Health check',
'action_cron_list' => 'List cron jobs',
],
'fr' => [
'title' => 'Controle Web OSMTool',
'subtitle' => 'Interface web securisee pour les actions OpenSim',
'login_title' => 'Connexion',
'password' => 'Mot de passe',
'login' => 'Se connecter',
'logout' => 'Se deconnecter',
'language' => 'Langue',
'profile' => 'Profil',
'action' => 'Action',
'execute' => 'Executer',
'workdir' => 'Repertoire de travail',
'output' => 'Sortie',
'status_ok' => 'Succes',
'status_error' => 'Erreur',
'invalid_login' => 'Echec de connexion.',
'csrf_error' => 'Jeton de formulaire invalide.',
'not_allowed' => 'Action non autorisee.',
'run_failed' => 'Execution echouee.',
'hint' => 'Seules les actions en liste blanche sont executees.',
'action_start' => 'Demarrer',
'action_stop' => 'Arreter',
'action_restart' => 'Redemarrer',
'action_smoke' => 'Test smoke',
'action_report' => 'Generer un rapport',
'action_health' => 'Controle de sante',
'action_cron_list' => 'Lister les cron jobs',
],
'es' => [
'title' => 'Control Web OSMTool',
'subtitle' => 'Interfaz web segura para acciones de OpenSim',
'login_title' => 'Inicio de sesion',
'password' => 'Contrasena',
'login' => 'Entrar',
'logout' => 'Salir',
'language' => 'Idioma',
'profile' => 'Perfil',
'action' => 'Accion',
'execute' => 'Ejecutar',
'workdir' => 'Directorio de trabajo',
'output' => 'Salida',
'status_ok' => 'Correcto',
'status_error' => 'Error',
'invalid_login' => 'Error de inicio de sesion.',
'csrf_error' => 'Token de formulario invalido.',
'not_allowed' => 'Accion no permitida.',
'run_failed' => 'La ejecucion fallo.',
'hint' => 'Solo se ejecutan acciones permitidas.',
'action_start' => 'Iniciar',
'action_stop' => 'Detener',
'action_restart' => 'Reiniciar',
'action_smoke' => 'Prueba smoke',
'action_report' => 'Generar informe',
'action_health' => 'Chequeo de salud',
'action_cron_list' => 'Listar cron jobs',
],
];
}
function detect_lang(array $supported): string
{
$lang = $_GET['lang'] ?? $_POST['lang'] ?? $_SESSION['lang'] ?? 'de';
if (!is_string($lang) || !in_array($lang, $supported, true)) {
return 'de';
}
return $lang;
}
function t(string $lang, string $key): string
{
$all = translations();
return $all[$lang][$key] ?? $all['en'][$key] ?? $key;
}
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
function allowed_profiles(): array
{
return ['grid-sim', 'robust', 'standalone'];
}
function allowed_actions(): array
{
return ['start', 'stop', 'restart', 'smoke', 'report', 'health', 'cron-list'];
}
function build_command(array $config, string $action, string $profile, string $lang, string $workdir): array
{
$runnerPath = (string)($config['runner_path'] ?? '');
if ($runnerPath === '') {
throw new RuntimeException('runner_path missing in config.');
}
$base = [];
$useSudo = (bool)($config['use_sudo'] ?? false);
if ($useSudo) {
$sudoUser = (string)($config['sudo_user'] ?? '');
if ($sudoUser === '') {
throw new RuntimeException('sudo_user missing in config.');
}
$base = ['sudo', '-n', '-u', $sudoUser];
}
return array_merge($base, [$runnerPath, $action, $profile, $lang, $workdir]);
}
function run_whitelisted_action(array $config, string $action, string $profile, string $lang, string $workdir): array
{
if (!in_array($action, allowed_actions(), true)) {
return ['ok' => false, 'exitCode' => 2, 'output' => "ERROR: action not allowed\n"];
}
if (!in_array($profile, allowed_profiles(), true)) {
return ['ok' => false, 'exitCode' => 2, 'output' => "ERROR: invalid profile\n"];
}
if (!in_array($lang, supported_languages(), true)) {
return ['ok' => false, 'exitCode' => 2, 'output' => "ERROR: invalid language\n"];
}
$workdirClean = trim($workdir);
if ($workdirClean === '') {
$workdirClean = (string)($config['default_workdir'] ?? '/opt');
}
$cmd = build_command($config, $action, $profile, $lang, $workdirClean);
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($cmd, $descriptorSpec, $pipes);
if (!is_resource($process)) {
return ['ok' => false, 'exitCode' => 1, 'output' => "ERROR: unable to start process\n"];
}
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
$output = (string)$stdout . (string)$stderr;
return ['ok' => $exitCode === 0, 'exitCode' => $exitCode, 'output' => $output];
}