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:
@@ -1,9 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Livewire\Attributes\On;
|
||||
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
||||
use Paymenter\Extensions\Servers\Proxmox\Models\ServerStatistics;
|
||||
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
||||
@@ -17,23 +15,9 @@ class Overview extends Component
|
||||
public $isEditingHostname = false;
|
||||
public $hostnameError = '';
|
||||
|
||||
public $password = '';
|
||||
public $password_confirmation = '';
|
||||
public $isEditingPassword = false;
|
||||
public $passwordError = '';
|
||||
|
||||
public string $cpuTimeRange = 'hour';
|
||||
public string $cpuAggregation = 'average';
|
||||
|
||||
public string $memoryTimeRange = 'hour';
|
||||
public string $memoryAggregation = 'average';
|
||||
|
||||
public string $networkTimeRange = 'hour';
|
||||
public string $networkAggregation = 'average';
|
||||
|
||||
public string $diskTimeRange = 'hour';
|
||||
public string $diskAggregation = 'average';
|
||||
|
||||
public $notes = '';
|
||||
public $isEditingNotes = false;
|
||||
public $notesError = '';
|
||||
@@ -46,7 +30,12 @@ class Overview extends Component
|
||||
$this->loadVmStatus();
|
||||
}
|
||||
|
||||
$this->hostname = $server->hostname ?? ($this->status->name ?? '');
|
||||
$this->hostname = $server->hostname ?? ($this->status?->name ?? '');
|
||||
|
||||
$notice = request()->query('notice');
|
||||
if ($notice === 'display_unavailable') {
|
||||
$this->notify('Display settings are only available for virtual machines.', 'info');
|
||||
}
|
||||
|
||||
// Load notes from Proxmox VM description
|
||||
try {
|
||||
@@ -68,136 +57,6 @@ class Overview extends Component
|
||||
}
|
||||
}
|
||||
|
||||
#[On('proxmox:stats')]
|
||||
public function stats()
|
||||
{
|
||||
// Group graph types by their (timeRange, aggregation) to batch API calls.
|
||||
// The Proxmox /rrddata endpoint returns ALL metrics in a single response,
|
||||
// so we can serve multiple graph types from one API call when they share parameters.
|
||||
$groups = [];
|
||||
$graphConfigs = [
|
||||
'cpu' => [$this->cpuTimeRange, $this->cpuAggregation],
|
||||
'memory' => [$this->memoryTimeRange, $this->memoryAggregation],
|
||||
'network' => [$this->networkTimeRange, $this->networkAggregation],
|
||||
'disk' => [$this->diskTimeRange, $this->diskAggregation],
|
||||
];
|
||||
|
||||
foreach ($graphConfigs as $graphType => [$timeRange, $aggregation]) {
|
||||
$key = $timeRange . '|' . $aggregation;
|
||||
$groups[$key]['timeRange'] = $timeRange;
|
||||
$groups[$key]['aggregation'] = $aggregation;
|
||||
$groups[$key]['types'][] = $graphType;
|
||||
}
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$this->fetchGraphDataBatch($group['types'], $group['timeRange'], $group['aggregation']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch rrddata once and dispatch to multiple graph types that share the same parameters.
|
||||
*/
|
||||
protected function fetchGraphDataBatch(array $graphTypes, string $timeRange, string $aggregation): void
|
||||
{
|
||||
$allowedRanges = ['hour', 'day', 'week', 'month', 'year'];
|
||||
$allowedAggs = ['average', 'maximum'];
|
||||
if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a shared cache key for the raw API data (not per-graph-type)
|
||||
$cacheKey = 'proxmox-stats-' . $this->server->id . '-rrd-' . $timeRange . '-' . $aggregation;
|
||||
$data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) {
|
||||
$proxmox = new Proxmox;
|
||||
$vmType = $this->server->node->vmType();
|
||||
$stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [
|
||||
'timeframe' => $timeRange,
|
||||
'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE',
|
||||
]);
|
||||
|
||||
return $stats->json()['data'] ?? [];
|
||||
});
|
||||
|
||||
foreach ($graphTypes as $graphType) {
|
||||
// Also store per-graph-type cache keys for individual update/invalidation
|
||||
$perGraphKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation;
|
||||
Cache::put($perGraphKey, $data, 60);
|
||||
|
||||
$this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data);
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetchGraphData(string $graphType, string $timeRange, string $aggregation)
|
||||
{
|
||||
$allowedRanges = ['hour', 'day', 'week', 'month', 'year'];
|
||||
$allowedAggs = ['average', 'maximum'];
|
||||
if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation;
|
||||
$data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) {
|
||||
$proxmox = new Proxmox;
|
||||
$vmType = $this->server->node->vmType();
|
||||
$stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [
|
||||
'timeframe' => $timeRange,
|
||||
'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE',
|
||||
]);
|
||||
|
||||
return $stats->json()['data'] ?? [];
|
||||
});
|
||||
|
||||
$this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data);
|
||||
}
|
||||
|
||||
public function updatedCpuTimeRange()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation);
|
||||
$this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation);
|
||||
}
|
||||
|
||||
public function updatedCpuAggregation()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation);
|
||||
$this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation);
|
||||
}
|
||||
|
||||
public function updatedMemoryTimeRange()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation);
|
||||
$this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation);
|
||||
}
|
||||
|
||||
public function updatedMemoryAggregation()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation);
|
||||
$this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation);
|
||||
}
|
||||
|
||||
public function updatedNetworkTimeRange()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation);
|
||||
$this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation);
|
||||
}
|
||||
|
||||
public function updatedNetworkAggregation()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation);
|
||||
$this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation);
|
||||
}
|
||||
|
||||
public function updatedDiskTimeRange()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation);
|
||||
$this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation);
|
||||
}
|
||||
|
||||
public function updatedDiskAggregation()
|
||||
{
|
||||
Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation);
|
||||
$this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation);
|
||||
}
|
||||
|
||||
protected function formatSeconds(?int $seconds): string
|
||||
{
|
||||
if (is_null($seconds) || $seconds < 0) {
|
||||
@@ -247,11 +106,11 @@ class Overview extends Component
|
||||
$tasks = $response->json()['data'] ?? [];
|
||||
foreach ($tasks as $task) {
|
||||
$type = strtolower($task['type'] ?? '');
|
||||
if (in_array($type, ['qmstart', 'qmresume'], true)) {
|
||||
if (in_array($type, ['qmstart', 'qmresume', 'pctstart', 'pctresume'], true)) {
|
||||
$counts['starts']++;
|
||||
} elseif (in_array($type, ['qmstop', 'qmshutdown'], true)) {
|
||||
} elseif (in_array($type, ['qmstop', 'qmshutdown', 'pctstop', 'pctshutdown'], true)) {
|
||||
$counts['shutdowns']++;
|
||||
} elseif (in_array($type, ['qmreboot', 'qmreset'], true)) {
|
||||
} elseif (in_array($type, ['qmreboot', 'qmreset', 'pctreboot', 'pctreset'], true)) {
|
||||
$counts['reboots']++;
|
||||
}
|
||||
}
|
||||
@@ -334,14 +193,14 @@ class Overview extends Component
|
||||
public function startEditingHostname()
|
||||
{
|
||||
$this->isEditingHostname = true;
|
||||
$this->hostname = $this->server->hostname ?? $this->status->name ?? '';
|
||||
$this->hostname = $this->server->hostname ?? $this->status?->name ?? '';
|
||||
$this->hostnameError = '';
|
||||
}
|
||||
|
||||
public function cancelEditingHostname()
|
||||
{
|
||||
$this->isEditingHostname = false;
|
||||
$this->hostname = $this->server->hostname ?? $this->status->name ?? '';
|
||||
$this->hostname = $this->server->hostname ?? $this->status?->name ?? '';
|
||||
$this->hostnameError = '';
|
||||
}
|
||||
|
||||
@@ -423,26 +282,21 @@ class Overview extends Component
|
||||
public function startEditingPassword()
|
||||
{
|
||||
$this->isEditingPassword = true;
|
||||
$this->password = '';
|
||||
$this->password_confirmation = '';
|
||||
$this->passwordError = '';
|
||||
}
|
||||
|
||||
public function cancelEditingPassword()
|
||||
{
|
||||
$this->isEditingPassword = false;
|
||||
$this->password = '';
|
||||
$this->password_confirmation = '';
|
||||
$this->passwordError = '';
|
||||
}
|
||||
|
||||
public function updatePassword()
|
||||
protected function validatePasswordInput(string $password, string $passwordConfirmation): bool
|
||||
{
|
||||
// Validate password using same rules as registration
|
||||
$validator = Validator::make(
|
||||
[
|
||||
'password' => $this->password,
|
||||
'password_confirmation' => $this->password_confirmation,
|
||||
'password' => $password,
|
||||
'password_confirmation' => $passwordConfirmation,
|
||||
],
|
||||
[
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
@@ -455,49 +309,49 @@ class Overview extends Component
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function closePasswordEditor(): void
|
||||
{
|
||||
$this->isEditingPassword = false;
|
||||
$this->passwordError = '';
|
||||
}
|
||||
|
||||
public function updatePassword(string $password = '', string $passwordConfirmation = '')
|
||||
{
|
||||
if (!$this->validatePasswordInput($password, $passwordConfirmation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update password in database
|
||||
$this->server->password = $this->password;
|
||||
$this->server->password = $password;
|
||||
$this->server->save();
|
||||
|
||||
// Refresh the server model to get latest data
|
||||
$this->server->refresh();
|
||||
|
||||
$this->notify('Password saved to database successfully', 'success');
|
||||
$this->isEditingPassword = false;
|
||||
$this->password = '';
|
||||
$this->password_confirmation = '';
|
||||
$this->passwordError = '';
|
||||
$this->closePasswordEditor();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->passwordError = 'Failed to save password: ' . $e->getMessage();
|
||||
report($e);
|
||||
$this->passwordError = 'Failed to save password. Please try again.';
|
||||
$this->notify('Failed to save password', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function applyPassword()
|
||||
public function applyPassword(string $password = '', string $passwordConfirmation = '')
|
||||
{
|
||||
// Validate password first
|
||||
$validator = Validator::make(
|
||||
[
|
||||
'password' => $this->password,
|
||||
'password_confirmation' => $this->password_confirmation,
|
||||
],
|
||||
[
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors = $validator->errors()->all();
|
||||
$this->passwordError = implode(' ', $errors);
|
||||
if (!$this->validatePasswordInput($password, $passwordConfirmation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update password in database first
|
||||
$this->server->password = $this->password;
|
||||
$this->server->password = $password;
|
||||
$this->server->save();
|
||||
|
||||
// Refresh the server model to get latest data
|
||||
@@ -506,10 +360,7 @@ class Overview extends Component
|
||||
// LXC containers don't support password change via config API
|
||||
if ($this->server->node->is_lxc) {
|
||||
$this->notify('Password saved to database. Note: LXC container passwords cannot be changed via API - use console access to change root password if needed.', 'warning');
|
||||
$this->isEditingPassword = false;
|
||||
$this->password = '';
|
||||
$this->password_confirmation = '';
|
||||
$this->passwordError = '';
|
||||
$this->closePasswordEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,17 +370,15 @@ class Overview extends Component
|
||||
|
||||
// For KVM: cipassword sets the password via cloud-init on next boot
|
||||
$response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [
|
||||
'cipassword' => $this->password,
|
||||
'cipassword' => $password,
|
||||
]);
|
||||
|
||||
// The request() method throws on error, so if we get here, it was successful
|
||||
$this->notify('Password applied to Proxmox successfully. The new password will be configured on the next boot via cloud-init. Please reboot your server for the changes to take effect.', 'success');
|
||||
$this->isEditingPassword = false;
|
||||
$this->password = '';
|
||||
$this->password_confirmation = '';
|
||||
$this->passwordError = '';
|
||||
$this->closePasswordEditor();
|
||||
} catch (\Exception $e) {
|
||||
$this->passwordError = 'Failed to apply password: ' . $e->getMessage();
|
||||
report($e);
|
||||
$this->passwordError = 'Failed to apply password. Please try again.';
|
||||
$this->notify('Failed to apply password to Proxmox', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user