fix(proxmox): harden console sessions and firewall rollout safety

Reduce security and reliability risk across the Proxmox extension by removing password exposure from Livewire state, hardening console auth/cookie handling, and improving sanitized error paths. Improve firewall and router performance by adding safer lazy loading, bounded pagination, cross-server compatibility guards, and lower-overhead API workflows.

Made-with: Cursor
This commit is contained in:
2026-03-28 19:34:46 -04:00
parent 9f996035db
commit 68043f0416
39 changed files with 1106 additions and 5013 deletions

View File

@@ -134,13 +134,14 @@ class Storage extends Component
public string $reattachTargetBus = 'scsi';
/**
* Storage name for cephpool.
* Active storage pool used for new drive allocations.
*/
protected string $storagePool = 'cephpool';
public function mount(Server $server): void
{
parent::mount($server);
$this->storagePool = trim((string) ($this->server->node->storage_location ?? '')) ?: 'cephpool';
$this->loadMaxAllowedStorage();
$this->loadStorageConfiguration(false);
@@ -181,7 +182,8 @@ class Storage extends Component
}
} catch (\Exception $e) {
if ($notify) {
$this->notify('Failed to load storage configuration: ' . $e->getMessage(), 'error');
report($e);
$this->notify('Failed to load storage configuration. Please refresh and try again.', 'error');
}
}
}
@@ -193,11 +195,27 @@ class Storage extends Component
{
$drives = [];
$totalSize = 0;
$isLxc = (bool) ($this->server->node->is_lxc ?? false);
foreach ($config as $key => $value) {
// Skip non-disk entries
if (!preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $key, $matches)) {
continue;
if ($isLxc) {
if (preg_match('/^rootfs$/', (string) $key)) {
$busType = 'rootfs';
$deviceIndex = 0;
} elseif (preg_match('/^mp(\d+)$/', (string) $key, $matches)) {
$busType = 'mp';
$deviceIndex = (int) $matches[1];
} else {
continue;
}
} else {
// Skip non-disk entries
if (!preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', (string) $key, $matches)) {
continue;
}
$busType = $matches[1];
$deviceIndex = (int) $matches[2];
}
// Skip cloud-init, EFI, and TPM disks
@@ -210,9 +228,6 @@ class Storage extends Component
continue;
}
$busType = $matches[1];
$deviceIndex = (int) $matches[2];
$diskInfo = $this->parseDiskString($value, $busType, $deviceIndex);
if ($diskInfo) {
$drives[$key] = $diskInfo;
@@ -222,10 +237,17 @@ class Storage extends Component
// Sort drives by bus type and index
uksort($drives, function ($a, $b) {
preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $a, $matchA);
preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $b, $matchB);
$busOrder = ['ide' => 0, 'sata' => 1, 'scsi' => 2, 'virtio' => 3];
preg_match('/^(ide|sata|scsi|virtio|mp)(\d+)$/', (string) $a, $matchA);
preg_match('/^(ide|sata|scsi|virtio|mp)(\d+)$/', (string) $b, $matchB);
if ((string) $a === 'rootfs') {
$matchA = ['rootfs', 'rootfs', '0'];
}
if ((string) $b === 'rootfs') {
$matchB = ['rootfs', 'rootfs', '0'];
}
$busOrder = ['rootfs' => 0, 'ide' => 1, 'sata' => 2, 'scsi' => 3, 'virtio' => 4, 'mp' => 5];
$busCompare = ($busOrder[$matchA[1]] ?? 4) <=> ($busOrder[$matchB[1]] ?? 4);
if ($busCompare !== 0) {
@@ -281,7 +303,7 @@ class Storage extends Component
$volume = trim($value);
// Extract storage name from volume (format: storage:volume-name)
$storageName = 'cephpool';
$storageName = '';
if (str_contains($volume, ':')) {
[$storageName, ] = explode(':', $volume, 2);
}
@@ -289,35 +311,37 @@ class Storage extends Component
// Try to get the volume size from the storage API
$sizeGb = 0;
$sizeFormatted = 'Unknown';
try {
$proxmox = new Proxmox;
$storageContent = $proxmox->request(
'/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content',
data: ['vmid' => $this->server->vm_id]
);
if ($storageName !== '') {
try {
$proxmox = new Proxmox;
$storageContent = $proxmox->request(
'/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content',
data: ['vmid' => $this->server->vm_id]
);
$data = $storageContent->json()['data'] ?? [];
if (is_array($data)) {
foreach ($data as $item) {
if (isset($item['volid']) && $item['volid'] === $volume) {
// Size is in bytes
if (isset($item['size'])) {
$sizeGb = round($item['size'] / (1024 * 1024 * 1024), 2);
$sizeFormatted = $sizeGb . ' GB';
$data = $storageContent->json()['data'] ?? [];
if (is_array($data)) {
foreach ($data as $item) {
if (isset($item['volid']) && $item['volid'] === $volume) {
// Size is in bytes
if (isset($item['size'])) {
$sizeGb = round($item['size'] / (1024 * 1024 * 1024), 2);
$sizeFormatted = $sizeGb . ' GB';
}
break;
}
break;
}
}
} catch (\Exception $e) {
// If we can't fetch size, leave as unknown
}
} catch (\Exception $e) {
// If we can't fetch size, leave as unknown
}
return [
'key' => 'unused' . $unusedIndex,
'index' => $unusedIndex,
'volume' => $volume,
'storage' => $storageName,
'storage' => $storageName !== '' ? $storageName : 'Unknown',
'size' => $sizeGb,
'size_formatted' => $sizeFormatted,
'label' => 'Detached Disk ' . $unusedIndex,
@@ -445,6 +469,8 @@ class Storage extends Component
protected function getDiskLabel(string $busType, int $index): string
{
$busLabels = [
'rootfs' => 'Root Filesystem',
'mp' => 'Mount Point',
'ide' => 'IDE',
'sata' => 'SATA',
'scsi' => 'SCSI',
@@ -584,7 +610,8 @@ class Storage extends Component
$this->closeAddDriveModal();
$this->loadStorageConfiguration(false);
} catch (\Exception $e) {
$this->notify('Failed to add drive: ' . $e->getMessage(), 'error');
report($e);
$this->notify('Failed to add drive. Please try again.', 'error');
}
}
@@ -735,7 +762,8 @@ class Storage extends Component
$this->closeEditDriveModal();
$this->loadStorageConfiguration(false);
} catch (\Exception $e) {
$this->notify('Failed to update drive settings: ' . $e->getMessage(), 'error');
report($e);
$this->notify('Failed to update drive settings. Please try again.', 'error');
}
}
@@ -853,7 +881,8 @@ class Storage extends Component
// Dispatch browser event to refresh the page
$this->dispatch('refresh-page');
} catch (\Exception $e) {
$this->notify('Failed to detach drive: ' . $e->getMessage(), 'error');
report($e);
$this->notify('Failed to detach drive. Please try again.', 'error');
}
}