From 45a18c0ecb364c42307641b4057ff5a814e69b2e Mon Sep 17 00:00:00 2001 From: Leonardo Bishop Date: Fri, 25 Apr 2025 00:54:15 +0100 Subject: Version control --- .gitignore | 2 + authenticate.php | 35 ++++++++++++++++ index.php | 22 ++++++++++ key.php.example | 4 ++ manage.php | 68 +++++++++++++++++++++++++++++++ mount.php | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ serviceDefinitions.php | 52 ++++++++++++++++++++++++ status.php | 72 +++++++++++++++++++++++++++++++++ styles.css | 50 +++++++++++++++++++++++ util.php | 94 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 506 insertions(+) create mode 100644 .gitignore create mode 100644 authenticate.php create mode 100644 index.php create mode 100644 key.php.example create mode 100644 manage.php create mode 100644 mount.php create mode 100644 serviceDefinitions.php create mode 100644 status.php create mode 100644 styles.css create mode 100644 util.php 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 @@ + + + + + + Authenticate + + + + + +
+

Authenticate

+
+

Please enter the secret key to continue.

+
+ Key: + +
+
+ diff --git a/index.php b/index.php new file mode 100644 index 0000000..d828e0f --- /dev/null +++ b/index.php @@ -0,0 +1,22 @@ + + + + + Welcome to bongo + + + + + +
+

Welcome to bongo

+ +
+ Get status +
+ 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 @@ + + + + + + Manage container + + + + + +
+

Manage container:

+ Home + Status +
+ 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); + ?> +

+

+ Status as reported by Docker + +
+

+ +

+ [Logs] + [Start] + [Stop] + [Restart] +

+
+ diff --git a/mount.php b/mount.php new file mode 100644 index 0000000..fa602d8 --- /dev/null +++ b/mount.php @@ -0,0 +1,107 @@ + + + + + + Mount LUKS device for <?php echo $service ?> + + + + + +
+

Mount LUKS device for

+ Home + Status +
+ 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 '

Attach /dev/' . $luksDevice->deviceName . ' with UUID="' . $luksDevice->uuid . '" to continue.

'; + 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 '

Cannot continue. Close luks device /dev/' . $luksDevice->deviceName . ' first.

'; + return; + } + Util\createBanner('✗', '/dev/' . $luksDevice->deviceName . ' is locked', 'bad'); + echo "

"; + echo "Provide the encryption key for /dev/" . $luksDevice->deviceName . " (" . $luksDevice->uuid . ")"; + echo "

"; + echo "
"; + echo "
"; + echo "Unlock /dev/" . $luksDevice->deviceName . ""; + echo ""; + echo "

"; + echo ""; + echo "
"; + echo "
"; + 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 "

Mount /dev/mapper/" . $luksDevice->mountPoint . " at /mnt/" . $luksDevice->mountPoint . ".

"; + echo "

[Mount device]

"; + return; + } else { + Util\createBanner('✓', '/dev/mapper/' . $luksDevice->mountPoint . ' is mounted at /mnt/' . $luksDevice->mountPoint, 'good'); + } + ?> +

There is nothing to do.

+
+ diff --git a/serviceDefinitions.php b/serviceDefinitions.php new file mode 100644 index 0000000..84f2216 --- /dev/null +++ b/serviceDefinitions.php @@ -0,0 +1,52 @@ +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 @@ + + + + + + Bongo status + + + + + +
+

Bongo status

+ Home + +
+ ' . $service->prettyName . ''; + $containers = []; + if (is_array($service->containerName)) { + $containers = $service->containerName; + } else { + $containers = [$service->containerName]; + } + ?> + luks !== null) : ?> + 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'); + } + ?> +

+ [Mount device or provide encryption key] +

+

+

+ Output + + + + +
+

+ + + + isNotFound === false) : ?> +

+ [Manage container] +

+

+

+ Status as reported by Docker + + +
+

+ + + +
+ 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 @@ +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 (''); + echo (''); + echo (''); + echo (''); + echo (''); + echo (''); + echo (''); + echo (''); + echo (''); + echo ('
Container IDNameStatusStarted atFinished at
' . $status->containerId . '' . $status->name . '' . $status->status . '' . $status->startedAt . '' . $status->finishedAt . '
'); +} + +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 ('
'); + echo ("

$symbol $message

"); + echo ('
'); +} + +function doShellExec($command, $redirect, $action) +{ + $output = shell_exec($command); + //if (empty($output)) { + // header("Location: $redirect"); + // exit; + //} + echo "

Output of $action

"; + echo "
$output
"; + echo "

[Acknowledge]

"; + 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; +} -- cgit v1.2.3-70-g09d2