aboutsummaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2025-04-25 01:13:03 +0100
committerLeonardo Bishop <me@leonardobishop.com>2025-04-25 01:13:03 +0100
commitc86f2b723e6956a6544bf98dc5011bd303280c6e (patch)
treef889fc105517e8a83863de621aa18a48e1231565 /bin
parent45a18c0ecb364c42307641b4057ff5a814e69b2e (diff)
Restructure repository
Diffstat (limited to 'bin')
-rw-r--r--bin/authenticate.php35
-rw-r--r--bin/index.php22
-rw-r--r--bin/key.php.example4
-rw-r--r--bin/manage.php68
-rw-r--r--bin/mount.php107
-rw-r--r--bin/serviceDefinitions.php52
-rw-r--r--bin/status.php72
-rw-r--r--bin/styles.css50
-rw-r--r--bin/util.php94
9 files changed, 504 insertions, 0 deletions
diff --git a/bin/authenticate.php b/bin/authenticate.php
new file mode 100644
index 0000000..7decfb5
--- /dev/null
+++ b/bin/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/bin/index.php b/bin/index.php
new file mode 100644
index 0000000..d828e0f
--- /dev/null
+++ b/bin/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/bin/key.php.example b/bin/key.php.example
new file mode 100644
index 0000000..94610ad
--- /dev/null
+++ b/bin/key.php.example
@@ -0,0 +1,4 @@
+<?php
+
+$superSecretToken = "abcd";
+
diff --git a/bin/manage.php b/bin/manage.php
new file mode 100644
index 0000000..c4858ca
--- /dev/null
+++ b/bin/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/bin/mount.php b/bin/mount.php
new file mode 100644
index 0000000..fa602d8
--- /dev/null
+++ b/bin/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/bin/serviceDefinitions.php b/bin/serviceDefinitions.php
new file mode 100644
index 0000000..84f2216
--- /dev/null
+++ b/bin/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/bin/status.php b/bin/status.php
new file mode 100644
index 0000000..3789d9e
--- /dev/null
+++ b/bin/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/bin/styles.css b/bin/styles.css
new file mode 100644
index 0000000..d984996
--- /dev/null
+++ b/bin/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/bin/util.php b/bin/util.php
new file mode 100644
index 0000000..b5cb9c7
--- /dev/null
+++ b/bin/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;
+}