diff options
| author | Leonardo Bishop <me@leonardobishop.com> | 2025-04-25 00:54:15 +0100 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.com> | 2025-04-25 00:54:15 +0100 |
| commit | 45a18c0ecb364c42307641b4057ff5a814e69b2e (patch) | |
| tree | 3ede8191b4b2ddb55df393d5b70d02977f4d8ea4 | |
Version control
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | authenticate.php | 35 | ||||
| -rw-r--r-- | index.php | 22 | ||||
| -rw-r--r-- | key.php.example | 4 | ||||
| -rw-r--r-- | manage.php | 68 | ||||
| -rw-r--r-- | mount.php | 107 | ||||
| -rw-r--r-- | serviceDefinitions.php | 52 | ||||
| -rw-r--r-- | status.php | 72 | ||||
| -rw-r--r-- | styles.css | 50 | ||||
| -rw-r--r-- | util.php | 94 |
10 files changed, 506 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e30d6d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +key.php + diff --git a/authenticate.php b/authenticate.php new file mode 100644 index 0000000..7decfb5 --- /dev/null +++ b/authenticate.php @@ -0,0 +1,35 @@ +<?php +require_once('util.php'); +require_once('serviceDefinitions.php'); + +session_start(); + +$redirect = $_GET['redirect']; +$token = $_POST['token']; + +if (isset($_POST['token']) && $_POST['token'] === Util\getSuperSecretToken()) { + $_SESSION['token'] = $_POST['token']; + header('Location: ' . $redirect); + exit; +} +?> +<!DOCTYPE html> +<html> + +<head> + <title>Authenticate</title> + <link rel="stylesheet" type="text/css" href="styles.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body> + <div class="container"> + <h1>Authenticate</h1> + <hr> + <p>Please enter the secret key to continue.</p> + <form action="authenticate.php?redirect=<?php echo $redirect ?>" method="POST"> + Key: <input type="password" name="token"> + <input type="submit"> + </form> + </div> +</body> diff --git a/index.php b/index.php new file mode 100644 index 0000000..d828e0f --- /dev/null +++ b/index.php @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Welcome to bongo</title> + <link rel="stylesheet" type="text/css" href="styles.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body> + <div class="container"> + <h1>Welcome to bongo</h1> + <ul> + <li>Jellyfin (:8096) <a href="http://bongo.local:8096">[local]</a> <a href="http://bongo.int.leonardobishop.com:8096">[int.leonardobishop.com]</a></li> + <li>Jellyfin (https) (broken)</li> + <li>Nextcloud <a href="https://cloud.int.leonardobishop.com">[int.leonardobishop.com]</a></li> + <li>Vaultwarden <a href="https://vault.int.leonardobishop.com">[int.leonardobishop.com]</a></li> + </ul> + <hr> + <a href="/status.php">Get status</a> + </div> +</body> diff --git a/key.php.example b/key.php.example new file mode 100644 index 0000000..94610ad --- /dev/null +++ b/key.php.example @@ -0,0 +1,4 @@ +<?php + +$superSecretToken = "abcd"; + diff --git a/manage.php b/manage.php new file mode 100644 index 0000000..c4858ca --- /dev/null +++ b/manage.php @@ -0,0 +1,68 @@ +<?php +require_once('util.php'); +require_once('serviceDefinitions.php'); + +session_start(); + +$container = $_GET['container']; +$action = $_GET['action']; + +Util\doSessionCheck('manage.php?container=' . $container); +?> +<!DOCTYPE html> +<html> + +<head> + <title>Manage container</title> + <link rel="stylesheet" type="text/css" href="styles.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body> + <div class="container"> + <h1>Manage container: <?php echo $container ?></h1> + <a href="index.php">Home</a> + <a href="status.php">Status</a> + <hr> + <?php + //if (empty($container)) { + // Util\createBanner('✗', 'No service specified', 'bad'); + // return; + //} + //if (!in_array($service, array_map(function ($s) { + // return $s->name; + //}, $services))) { + // Util\createBanner('✗', "Service '$service' is unknown", 'bad'); + // return; + //} + + $status = Util\getDockerStatus($container); + + if ($status->status === '-') { + Util\createBanner('✗', "Container '$container' not found", 'bad'); + return; + } + + if ($action === 'start' || $action === 'stop' || $action === 'restart' || $action === 'logs') { + // if ($action === 'start' || $action === 'stop' || $action === 'restart') { + $safeService = escapeshellarg($container); + Util\doShellExec('sudo docker ' . $action . ' ' . $safeService, '/manage.php?container=' . $container, $action); + } + + Util\createStatusBanner($status); + ?> + <p> + <details> + <summary>Status as reported by Docker</summary> + <?php Util\createStatusTable($status); ?> + </details> + </p> + + <p class="control-list"> + <a href="manage.php?container=<?php echo $container ?>&action=logs">[Logs]</a> + <a href="manage.php?container=<?php echo $container ?>&action=start">[Start]</a> + <a href="manage.php?container=<?php echo $container ?>&action=stop">[Stop]</a> + <a href="manage.php?container=<?php echo $container ?>&action=restart">[Restart]</a> + </p> + </div> +</body> diff --git a/mount.php b/mount.php new file mode 100644 index 0000000..fa602d8 --- /dev/null +++ b/mount.php @@ -0,0 +1,107 @@ +<?php +require_once('util.php'); +require_once('serviceDefinitions.php'); + +session_start(); + +$service = $_GET['service']; + +Util\doSessionCheck('mount.php?service=' . $service); + +$serviceDefinition = ServiceDefinitions\getServiceDefinition($service); +?> +<!DOCTYPE html> +<html> + +<head> + <title>Mount LUKS device for <?php echo $service ?></title> + <link rel="stylesheet" type="text/css" href="styles.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body> + <div class="container"> + <h1>Mount LUKS device for <?php echo $service ?></h1> + <a href="index.php">Home</a> + <a href="status.php">Status</a> + <hr> + <?php + + if ($serviceDefinition === null) { + Util\createBanner('✗', "There is no service definition for '" . $service . "'", 'bad'); + return; + } + + $luksDevice = $serviceDefinition->luks; + + if ($luksDevice === null) { + Util\createBanner('✗', $service . ' has no LUKS device to mount', 'bad'); + return; + } + + $key = $_POST['key']; + $mount = $_GET['mount']; + + $disk = exec('blkid /dev/' . $luksDevice->deviceName . ' | grep "UUID=\"' . $luksDevice->uuid . '\""'); + $diskOk = !empty($disk); + + $cryptdevice = exec('lsblk -lno NAME,TYPE,MOUNTPOINT /dev/' . $luksDevice->deviceName . ' | grep "' . $luksDevice->mountPoint . '[[:space:]]*crypt"'); + $cryptdeviceOk = !empty($cryptdevice); + + $cryptdeviceMapping = exec('lsblk -lno NAME,TYPE,MOUNTPOINT /dev/' . $luksDevice->deviceName . ' | grep "crypt" | awk \'{print $1}\''); + + $mountpoint = exec('cat /proc/mounts | grep "/dev/mapper/' . $luksDevice->mountPoint . ' /mnt/' . $luksDevice->mountPoint . '"'); + $mountpointOk = !empty($mountpoint); + + if (!empty($key) && $diskOk && !$cryptdeviceOk) { + $safeKey = escapeshellarg($key); + Util\doShellExec('echo ' . $safeKey . ' | sudo cryptsetup --verbose luksOpen /dev/' . $luksDevice->deviceName . ' ' . $luksDevice->mountPoint . ' 2>&1', '/mount.php?service=' . $service, 'cryptsetup'); + } + + if (!empty($mount) && $diskOk && $cryptdeviceOk && !$mountpointOk) { + Util\doShellExec('sudo mount -v /dev/mapper/' . $luksDevice->mountPoint . ' /mnt/' . $luksDevice->mountPoint, '/mount.php?service=' . $service, 'mount'); + } + + if (!$diskOk) { + Util\createBanner('✗', '/dev/' . $luksDevice->deviceName . ' is not attached or has incorrect UUID', 'bad'); + echo '<p>Attach /dev/' . $luksDevice->deviceName . ' with UUID="' . $luksDevice->uuid . '" to continue.</p>'; + return; + } else { + Util\createBanner('✓', '/dev/' . $luksDevice->deviceName . ' is attached', 'good'); + } + + if (!$cryptdeviceOk) { + if (!empty($cryptdeviceMapping)) { + Util\createBanner('✗', "/dev/" . $luksDevice->deviceName . " has incorrect mapping '" . $cryptdeviceMapping. "'", 'bad'); + echo '<p>Cannot continue. Close luks device /dev/' . $luksDevice->deviceName . ' first.</p>'; + return; + } + Util\createBanner('✗', '/dev/' . $luksDevice->deviceName . ' is locked', 'bad'); + echo "<p>"; + echo "Provide the encryption key for /dev/" . $luksDevice->deviceName . " (" . $luksDevice->uuid . ")"; + echo "</p>"; + echo "<form method='POST'>"; + echo "<fieldset>"; + echo "<legend>Unlock /dev/" . $luksDevice->deviceName . "</legend>"; + echo "<label for='key'>Key: </label>"; + echo "<input type='password' id='key' name='key'><br><br>"; + echo "<input type='submit' value='Go'>"; + echo "</fieldset>"; + echo "</form>"; + return; + } else { + Util\createBanner('✓', "/dev/" . $luksDevice->deviceName . " is open and has mapping '" . $luksDevice->mountPoint . "'", 'good'); + } + + if (!$mountpointOk) { + Util\createBanner('✗', '/dev/mapper/' . $luksDevice->mountPoint . ' is not mounted at /mnt/' . $luksDevice->mountPoint, 'bad'); + echo "<p>Mount /dev/mapper/" . $luksDevice->mountPoint . " at /mnt/" . $luksDevice->mountPoint . ".</p>"; + echo "<p class='control-list'><a href='/mount.php?service=" . $service . "&mount=1'>[Mount device]</a></p>"; + return; + } else { + Util\createBanner('✓', '/dev/mapper/' . $luksDevice->mountPoint . ' is mounted at /mnt/' . $luksDevice->mountPoint, 'good'); + } + ?> + <p>There is nothing to do.</p> + </div> +</body> diff --git a/serviceDefinitions.php b/serviceDefinitions.php new file mode 100644 index 0000000..84f2216 --- /dev/null +++ b/serviceDefinitions.php @@ -0,0 +1,52 @@ +<?php +namespace ServiceDefinitions; + +class ServiceDefinition +{ + public $name; + public $prettyName; + public $containerName; + public $luks; + + public function __construct($name, $prettyName, $containerName, $luks) + { + $this->name = $name; + $this->prettyName = $prettyName; + $this->containerName = $containerName; + $this->luks = $luks; + } +} + +class LuksDataDisk +{ + public $deviceName; + public $uuid; + public $mountPoint; + + public function __construct($deviceName, $uuid, $mountPoint) + { + $this->deviceName = $deviceName; + $this->uuid = $uuid; + $this->mountPoint = $mountPoint; + } +} + +$services = [ + new ServiceDefinition('vaultwarden', 'Vaultwarden', 'vaultwarden', null), + new ServiceDefinition('nextcloud', 'Nextcloud', ['nextcloud', 'nextcloud_db'], new LuksDataDisk('mmcblk0p3', '19537ab9-d855-416c-99c3-ebc85e02bfbf', 'cloud')), + new ServiceDefinition('jellyfin', 'Jellyfin', 'jellyfin', new LuksDataDisk('sda', '12158df0-2738-4c32-a7b9-36c11dde427f', 'media')), + new ServiceDefinition('minecraft', 'Minecraft server', 'minecraft', null), +]; + +function getServiceDefinition($name) { + global $services; + + $matching = array_filter($services, function ($service) use ($name) { return $service->name === $name; }); + if (count($matching) === 0) { + return null; + } + return reset($matching); +} + +?> + diff --git a/status.php b/status.php new file mode 100644 index 0000000..3789d9e --- /dev/null +++ b/status.php @@ -0,0 +1,72 @@ +<?php +require_once('util.php'); +require_once('serviceDefinitions.php'); +?> +<!DOCTYPE html> +<html> + +<head> + <title>Bongo status</title> + <link rel="stylesheet" type="text/css" href="styles.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body> + <div class="container"> + <h1>Bongo status</h1> + <a href="index.php">Home</a> + <?php foreach ($services as $service) : ?> + <hr> + <?php + echo '<h2>' . $service->prettyName . '</h2>'; + $containers = []; + if (is_array($service->containerName)) { + $containers = $service->containerName; + } else { + $containers = [$service->containerName]; + } + ?> + <?php if ($service->luks !== null) : ?> + <?php + $luksDevice = $service->luks; + $mountpoint = exec('cat /proc/mounts | grep "/dev/mapper/' . $luksDevice->mountPoint . ' /mnt/' . $luksDevice->mountPoint . '"'); + if (empty($mountpoint)) { + Util\createBanner('✗', '/dev/mapper/' . $luksDevice->mountPoint . ' is not mounted at /mnt/' . $luksDevice->mountPoint, 'bad'); + } else { + Util\createBanner('✓', '/dev/mapper/' . $luksDevice->mountPoint . ' is mounted at /mnt/' . $luksDevice->mountPoint, 'good'); + } + ?> + <p class="control-list"> + <a href="mount.php?service=<?php echo $service->name ?>">[Mount device or provide encryption key]</a> + </p> + <p> + <details> + <summary>Output</summary> + + <code> + <?php echo $mountpoint ?> + </code> + </details> + </p> + <?php endif; ?> + <?php foreach ($containers as $containerName) : ?> + <?php + $status = Util\getDockerStatus($containerName); + Util\createStatusBanner($status); + ?> + <?php if ($status->isNotFound === false) : ?> + <p class="control-list"> + <a href="manage.php?container=<?php echo $containerName ?>">[Manage container]</a> + </p> + <p> + <details> + <summary>Status as reported by Docker</summary> + + <?php Util\createStatusTable($status) ?> + </details> + </p> + <?php endif; ?> + <?php endforeach; ?> + <?php endforeach; ?> + </div> +</body> diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..d984996 --- /dev/null +++ b/styles.css @@ -0,0 +1,50 @@ +html, body { + font-family: sans-serif; + +} +table { + border-collapse: collapse; +} + +th, td { + text-align: left; + padding: 8px; + border: black 1px solid; +} + +th { + background-color: #f2f2f2; +} + +a { + color: #0000EE; +} + +a :visited{ + color: #0000EE; +} + +.status-banner { + background-color: #f2f2f2; + padding: 0 5px; + border: 1px solid #e7e7e7; +} + +.status-banner.good { + background-color: #aaffaa; + border: 1px solid #90ee90; +} + +.status-banner.bad { + background-color: #ffcccb; + border: 1px solid #ff6666; +} + +.container { + max-width: 800px; + margin: 0 auto; +} + +.control-list a { + text-decoration: none; +}
\ No newline at end of file diff --git a/util.php b/util.php new file mode 100644 index 0000000..b5cb9c7 --- /dev/null +++ b/util.php @@ -0,0 +1,94 @@ +<?php +namespace Util; + +class ServiceStatus +{ + public $name; + public $containerId; + public $status; + public $startedAt; + public $finishedAt; + public $isNotFound; + + public function __construct($name, $containerId, $status, $startedAt, $finishedAt, $isNotFound) + { + $this->name = $name; + $this->containerId = $containerId; + $this->status = $status; + $this->startedAt = $startedAt; + $this->finishedAt = $finishedAt; + $this->isNotFound = $isNotFound; + } +} + +function getDockerStatus($containerName): ServiceStatus +{ + $dockerOutput = exec('sudo docker inspect --format=\'{{.Id}} {{.State.Status}} {{.State.StartedAt}} {{.State.FinishedAt}}\' ' . $containerName); + if (empty($dockerOutput)) { + return new ServiceStatus($containerName, '-', '-', '-', '-', true); + } + $parts = explode(' ', $dockerOutput); + $status = new ServiceStatus($containerName, substr($parts[0], 0, 12), $parts[1], $parts[2], $parts[3], false); + return $status; +} + +function createStatusTable(ServiceStatus $status) +{ + echo ('<table>'); + echo ('<tr><th>Container ID</th><th>Name</th><th>Status</th><th>Started at</th><th>Finished at</th></tr>'); + echo ('<tr>'); + echo ('<td>' . $status->containerId . '</td>'); + echo ('<td>' . $status->name . '</td>'); + echo ('<td>' . $status->status . '</td>'); + echo ('<td>' . $status->startedAt . '</td>'); + echo ('<td>' . $status->finishedAt . '</td>'); + echo ('</tr>'); + echo ('</table>'); +} + +function createStatusBanner(ServiceStatus $status) +{ + if ($status->isNotFound) { + createBanner('✗', "Container '" . $status->name . "' not found", 'bad'); + return; + } + $state = $status->status === 'running' ? 'good' : 'bad'; + $symbol = $status->status === 'running' ? '✓' : '✗'; + createBanner($symbol, "Status of '$status->name' is '$status->status'", $state); +} + +function createBanner($symbol, $message, $state) +{ + echo ('<div class="status-banner ' . $state . '">'); + echo ("<p><b>$symbol</b> $message</p>"); + echo ('</div>'); +} + +function doShellExec($command, $redirect, $action) +{ + $output = shell_exec($command); + //if (empty($output)) { + // header("Location: $redirect"); + // exit; + //} + echo "<p>Output of $action</p>"; + echo "<pre>$output</pre>"; + echo "<p class='control-list'><a href='$redirect'>[Acknowledge]</a></p>"; + exit; +} + +function doSessionCheck($redirect) +{ + if (!isset($_SESSION['token']) || $_SESSION['token'] !== getSuperSecretToken()) { + header('Location: authenticate.php?redirect=/' . $redirect); + exit; + } +} + +include('key.php'); + +function getSuperSecretToken() +{ + global $superSecretToken; + return $superSecretToken; +} |
