569 lines
22 KiB
PHP
569 lines
22 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
|
|
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
|
|
class Firewall extends Component
|
|
{
|
|
public bool $firewallEnabled = false;
|
|
public array $rules = [];
|
|
public array $aliases = [];
|
|
public array $ipsets = [];
|
|
public bool $loading = false;
|
|
public string $error = '';
|
|
public bool $showErrorModal = false;
|
|
public bool $showRuleForm = false;
|
|
|
|
// Rule creation/editing
|
|
public ?int $editingRulePos = null;
|
|
public string $ruleAction = 'ACCEPT';
|
|
public string $ruleSource = '';
|
|
public string $ruleDest = '';
|
|
public string $ruleProto = '';
|
|
public string $ruleDport = '';
|
|
public string $ruleSport = '';
|
|
public string $ruleComment = '';
|
|
public bool $ruleEnable = true;
|
|
public string $ruleMacro = '';
|
|
public string $ruleIface = '';
|
|
public string $ruleLog = '';
|
|
|
|
// Alias management
|
|
public string $aliasName = '';
|
|
public string $aliasCidr = '';
|
|
public string $aliasComment = '';
|
|
|
|
// IPSet management
|
|
public string $ipsetName = '';
|
|
public string $ipsetComment = '';
|
|
|
|
public function mount(Server $server): void
|
|
{
|
|
parent::mount($server);
|
|
$this->loadFirewallOptions();
|
|
$this->loadFirewallRules();
|
|
$this->loadAliases();
|
|
$this->loadIpsets();
|
|
}
|
|
|
|
/**
|
|
* Make a firewall API request using the dedicated firewall credentials
|
|
*/
|
|
protected function firewallRequest(string $url, string $method = 'get', array $data = []): \Illuminate\Http\Client\Response
|
|
{
|
|
$baseUrl = 'https://pve.virtnet.bond';
|
|
$user = 'root@pam';
|
|
$tokenId = 'firewall';
|
|
$secret = '0a439967-0a44-4b94-8906-c36b3a40cf68';
|
|
|
|
$fullUrl = rtrim($baseUrl, '/') . '/api2/json' . $url;
|
|
|
|
// Proxmox API token format: PVEAPIToken=USER@REALM!TOKENID=SECRET
|
|
$authHeader = 'PVEAPIToken=' . $user . '!' . $tokenId . '=' . $secret;
|
|
|
|
$request = Http::withHeaders([
|
|
'Authorization' => $authHeader,
|
|
'Accept' => 'application/json',
|
|
])->withOptions([
|
|
'verify' => true,
|
|
]);
|
|
|
|
// For PUT/POST requests, Proxmox API expects form data
|
|
if (in_array(strtolower($method), ['put', 'post']) && !empty($data)) {
|
|
return $request->asForm()->$method($fullUrl, $data)->throw();
|
|
}
|
|
|
|
return $request->$method($fullUrl, $data)->throw();
|
|
}
|
|
|
|
public function loadFirewallOptions(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->firewallEnabled = isset($data['enable']) && $data['enable'] == 1;
|
|
$this->error = '';
|
|
$this->showErrorModal = false;
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
// Try to extract more detailed error from response if available
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to load firewall options: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
$this->firewallEnabled = false;
|
|
}
|
|
}
|
|
|
|
public function toggleFirewall(): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
// Store the current state before toggling
|
|
$currentState = $this->firewallEnabled;
|
|
// Calculate the new desired state (opposite of current)
|
|
$newState = $currentState ? 0 : 1;
|
|
$payload = ['enable' => $newState];
|
|
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options',
|
|
'put',
|
|
$payload
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
// Reload from API to get the actual state (don't assume it worked)
|
|
$this->loadFirewallOptions();
|
|
$this->notify('Firewall ' . ($this->firewallEnabled ? 'enabled' : 'disabled') . ' successfully', 'success');
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->notify('Failed to toggle firewall: ' . $message, 'error');
|
|
// Reload to ensure UI matches actual state
|
|
$this->loadFirewallOptions();
|
|
}
|
|
} catch (\Illuminate\Http\Client\RequestException $e) {
|
|
$errorMessage = $e->getMessage();
|
|
// Try to extract more detailed error from response
|
|
if ($e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to toggle firewall: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
// Reload to ensure UI matches actual state
|
|
$this->loadFirewallOptions();
|
|
} catch (\Exception $e) {
|
|
$this->error = 'Failed to toggle firewall: ' . $e->getMessage();
|
|
$this->showErrorModal = true;
|
|
// Reload to ensure UI matches actual state
|
|
$this->loadFirewallOptions();
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadFirewallRules(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->rules = is_array($data) ? $data : [];
|
|
$this->error = '';
|
|
$this->showErrorModal = false;
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
// Try to extract more detailed error from response if available
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to load firewall rules: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
$this->rules = [];
|
|
}
|
|
}
|
|
|
|
public function startCreateRule(): void
|
|
{
|
|
$this->resetRuleForm();
|
|
$this->editingRulePos = null;
|
|
$this->showRuleForm = true;
|
|
}
|
|
|
|
public function startEditRule(int $pos): void
|
|
{
|
|
$rule = collect($this->rules)->firstWhere('pos', $pos);
|
|
if ($rule) {
|
|
$this->editingRulePos = $pos;
|
|
$this->showRuleForm = true;
|
|
$this->ruleAction = $rule['action'] ?? 'ACCEPT';
|
|
$this->ruleSource = $rule['source'] ?? '';
|
|
$this->ruleDest = $rule['dest'] ?? '';
|
|
$this->ruleProto = $rule['proto'] ?? '';
|
|
$this->ruleDport = $rule['dport'] ?? '';
|
|
$this->ruleSport = $rule['sport'] ?? '';
|
|
$this->ruleComment = $rule['comment'] ?? '';
|
|
$this->ruleEnable = !isset($rule['enable']) || $rule['enable'] == 1;
|
|
$this->ruleMacro = $rule['macro'] ?? '';
|
|
$this->ruleIface = $rule['iface'] ?? '';
|
|
$this->ruleLog = $rule['log'] ?? '';
|
|
}
|
|
}
|
|
|
|
public function resetRuleForm(): void
|
|
{
|
|
$this->ruleAction = 'ACCEPT';
|
|
$this->ruleSource = '';
|
|
$this->ruleDest = '';
|
|
$this->ruleProto = '';
|
|
$this->ruleDport = '';
|
|
$this->ruleSport = '';
|
|
$this->ruleComment = '';
|
|
$this->ruleEnable = true;
|
|
$this->ruleMacro = '';
|
|
$this->ruleIface = '';
|
|
$this->ruleLog = '';
|
|
}
|
|
|
|
public function saveRule(): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
// Build payload - always include action, enable, and type
|
|
// Type defaults to 'in' for incoming rules (can be 'in' or 'out')
|
|
$payload = [
|
|
'action' => $this->ruleAction,
|
|
'enable' => $this->ruleEnable ? 1 : 0,
|
|
'type' => 'in', // Default to incoming rule
|
|
];
|
|
|
|
// Add optional parameters only if they have values
|
|
// Note: Proxmox allows rules with just action, but some parameters may be required in certain contexts
|
|
if (!empty(trim($this->ruleSource))) {
|
|
$payload['source'] = trim($this->ruleSource);
|
|
}
|
|
if (!empty(trim($this->ruleDest))) {
|
|
$payload['dest'] = trim($this->ruleDest);
|
|
}
|
|
if (!empty(trim($this->ruleProto))) {
|
|
$payload['proto'] = trim($this->ruleProto);
|
|
}
|
|
if (!empty(trim($this->ruleDport))) {
|
|
$payload['dport'] = trim($this->ruleDport);
|
|
}
|
|
if (!empty(trim($this->ruleSport))) {
|
|
$payload['sport'] = trim($this->ruleSport);
|
|
}
|
|
if (!empty(trim($this->ruleComment))) {
|
|
$payload['comment'] = trim($this->ruleComment);
|
|
}
|
|
if (!empty(trim($this->ruleMacro))) {
|
|
$payload['macro'] = trim($this->ruleMacro);
|
|
}
|
|
if (!empty(trim($this->ruleIface))) {
|
|
$payload['iface'] = trim($this->ruleIface);
|
|
}
|
|
if (!empty(trim($this->ruleLog))) {
|
|
$payload['log'] = trim($this->ruleLog);
|
|
}
|
|
|
|
if ($this->editingRulePos !== null) {
|
|
// Update existing rule
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $this->editingRulePos,
|
|
'put',
|
|
$payload
|
|
);
|
|
} else {
|
|
// Create new rule
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules',
|
|
'post',
|
|
$payload
|
|
);
|
|
}
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Rule ' . ($this->editingRulePos !== null ? 'updated' : 'created') . ' successfully', 'success');
|
|
$this->resetRuleForm();
|
|
$this->editingRulePos = null;
|
|
$this->showRuleForm = false;
|
|
$this->loadFirewallRules();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to save rule: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
$fullErrorDetails = '';
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors'])) {
|
|
// Try to get the full error details
|
|
$errorMessages = [];
|
|
foreach ($errorData['errors'] as $error) {
|
|
if (is_array($error)) {
|
|
$errorMessages[] = json_encode($error);
|
|
} else {
|
|
$errorMessages[] = $error;
|
|
}
|
|
}
|
|
$fullErrorDetails = implode(' ', $errorMessages);
|
|
$errorMessage = !empty($fullErrorDetails) ? $fullErrorDetails : ($errorData['errors'][0]['message'] ?? $errorMessage);
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
if (isset($errorData['errors'])) {
|
|
$errorMessage .= ' ' . json_encode($errorData['errors']);
|
|
}
|
|
}
|
|
}
|
|
$this->error = 'Failed to save rule: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function deleteRule(int $pos): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos,
|
|
'delete'
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Rule deleted successfully', 'success');
|
|
$this->loadFirewallRules();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to delete rule: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to delete rule: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadAliases(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->aliases = is_array($data) ? $data : [];
|
|
} catch (\Exception $e) {
|
|
$this->aliases = [];
|
|
}
|
|
}
|
|
|
|
public function createAlias(): void
|
|
{
|
|
if (empty($this->aliasName) || empty($this->aliasCidr)) {
|
|
$this->error = 'Name and CIDR are required';
|
|
$this->showErrorModal = true;
|
|
return;
|
|
}
|
|
|
|
$this->loading = true;
|
|
try {
|
|
$payload = [
|
|
'name' => $this->aliasName,
|
|
'cidr' => $this->aliasCidr,
|
|
];
|
|
if ($this->aliasComment) {
|
|
$payload['comment'] = $this->aliasComment;
|
|
}
|
|
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases',
|
|
'post',
|
|
$payload
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Alias created successfully', 'success');
|
|
$this->aliasName = '';
|
|
$this->aliasCidr = '';
|
|
$this->aliasComment = '';
|
|
$this->loadAliases();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to create alias: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to create alias: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function deleteAlias(string $name): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $name,
|
|
'delete'
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Alias deleted successfully', 'success');
|
|
$this->loadAliases();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to delete alias: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to delete alias: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadIpsets(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->ipsets = is_array($data) ? $data : [];
|
|
} catch (\Exception $e) {
|
|
$this->ipsets = [];
|
|
}
|
|
}
|
|
|
|
public function createIpset(): void
|
|
{
|
|
if (empty($this->ipsetName)) {
|
|
$this->error = 'Name is required';
|
|
$this->showErrorModal = true;
|
|
return;
|
|
}
|
|
|
|
$this->loading = true;
|
|
try {
|
|
$payload = [
|
|
'name' => $this->ipsetName,
|
|
];
|
|
if ($this->ipsetComment) {
|
|
$payload['comment'] = $this->ipsetComment;
|
|
}
|
|
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset',
|
|
'post',
|
|
$payload
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('IP set created successfully', 'success');
|
|
$this->ipsetName = '';
|
|
$this->ipsetComment = '';
|
|
$this->loadIpsets();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to create IP set: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to create IP set: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function deleteIpset(string $name): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $name,
|
|
'delete'
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('IP set deleted successfully', 'success');
|
|
$this->loadIpsets();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to delete IP set: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to delete IP set: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('proxmox::firewall')->layoutData([
|
|
'server' => $this->server,
|
|
'status' => $this->status,
|
|
]);
|
|
}
|
|
}
|
|
|