1489 lines
59 KiB
PHP
Executable File
1489 lines
59 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ServerManager\Controllers;
|
|
|
|
use ServerManager\Core\App;
|
|
use ServerManager\Core\Session;
|
|
use ServerManager\Core\Validator;
|
|
use ServerManager\Models\Server;
|
|
use ServerManager\Services\SSHService;
|
|
use ServerManager\Services\MonitoringService;
|
|
use ServerManager\Services\AuditService;
|
|
use ServerManager\Models\CommandHistory;
|
|
|
|
class ServerController
|
|
{
|
|
private \ServerManager\Core\View $view;
|
|
private Server $serverModel;
|
|
private AuditService $auditService;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->view = App::getInstance()->getView();
|
|
$this->serverModel = new Server();
|
|
$this->auditService = new AuditService();
|
|
}
|
|
|
|
public function index(): void
|
|
{
|
|
$page = (int) ($_GET['page'] ?? 1);
|
|
$search = $_GET['search'] ?? '';
|
|
$status = $_GET['status'] ?? '';
|
|
$group = $_GET['group'] ?? '';
|
|
|
|
$filters = [];
|
|
if ($search) $filters['search'] = $search;
|
|
if ($status) $filters['status'] = $status;
|
|
if ($group) $filters['group_name'] = $group;
|
|
|
|
$servers = $this->serverModel->getAll($page, 15, $filters);
|
|
$groups = $this->serverModel->getGroups();
|
|
|
|
$this->view->display('servers.index', [
|
|
'title' => 'Servers - ServerManager',
|
|
'servers' => $servers,
|
|
'groups' => $groups,
|
|
'search' => $search,
|
|
'statusFilter' => $status,
|
|
'groupFilter' => $group,
|
|
]);
|
|
}
|
|
|
|
public function create(): void
|
|
{
|
|
$groups = $this->serverModel->getGroups();
|
|
|
|
$this->view->display('servers.create', [
|
|
'title' => 'Add Server - ServerManager',
|
|
'groups' => $groups,
|
|
]);
|
|
}
|
|
|
|
public function store(): void
|
|
{
|
|
$validator = new Validator();
|
|
$rules = [
|
|
'name' => 'required|min:2|max:100',
|
|
'ip_address' => 'required',
|
|
'ssh_port' => 'required|numeric|port',
|
|
'ssh_user' => 'required|min:2|max:50',
|
|
'group_name' => 'max:50',
|
|
];
|
|
|
|
if (!$validator->validate($_POST, $rules)) {
|
|
Session::setFlash('error', $validator->getFirstError());
|
|
$this->view->redirect('/servers/create');
|
|
}
|
|
|
|
if (empty($_POST['ssh_password']) && empty($_POST['ssh_key'])) {
|
|
Session::setFlash('error', 'You must provide either an SSH password or a private key.');
|
|
$this->view->redirect('/servers/create');
|
|
}
|
|
|
|
$data = [
|
|
'name' => $_POST['name'],
|
|
'description' => $_POST['description'] ?? '',
|
|
'ip_address' => $_POST['ip_address'],
|
|
'ssh_port' => (int) $_POST['ssh_port'],
|
|
'ssh_user' => $_POST['ssh_user'],
|
|
'ssh_password' => $_POST['ssh_password'] ?? null,
|
|
'ssh_key' => $_POST['ssh_key'] ?? null,
|
|
'group_name' => $_POST['group_name'] ?? null,
|
|
'is_active' => (int) ($_POST['is_active'] ?? 1),
|
|
];
|
|
|
|
$id = $this->serverModel->create($data);
|
|
|
|
$this->auditService->log('server_created', 'server', $id, [
|
|
'name' => $data['name'],
|
|
'ip_address' => $data['ip_address'],
|
|
]);
|
|
|
|
Session::setFlash('success', 'Server added successfully.');
|
|
$this->view->redirect('/servers');
|
|
}
|
|
|
|
public function show(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
$monitoringService = new MonitoringService();
|
|
$latestMetrics = $monitoringService->getLatestMetrics($id);
|
|
$historicalMetrics = $monitoringService->getHistoricalMetrics($id, 24);
|
|
|
|
$this->view->display('servers.show', [
|
|
'title' => $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'metrics' => $latestMetrics,
|
|
'historicalMetrics' => $historicalMetrics,
|
|
]);
|
|
}
|
|
|
|
public function edit(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
$groups = $this->serverModel->getGroups();
|
|
|
|
$this->view->display('servers.edit', [
|
|
'title' => 'Edit ' . $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'groups' => $groups,
|
|
]);
|
|
}
|
|
|
|
public function update(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
$validator = new Validator();
|
|
$rules = [
|
|
'name' => 'required|min:2|max:100',
|
|
'ip_address' => 'required',
|
|
'ssh_port' => 'required|numeric|port',
|
|
'ssh_user' => 'required|min:2|max:50',
|
|
'group_name' => 'max:50',
|
|
];
|
|
|
|
if (!$validator->validate($_POST, $rules)) {
|
|
Session::setFlash('error', $validator->getFirstError());
|
|
$this->view->redirect("/servers/{$id}/edit");
|
|
}
|
|
|
|
$data = [
|
|
'name' => $_POST['name'],
|
|
'description' => $_POST['description'] ?? '',
|
|
'ip_address' => $_POST['ip_address'],
|
|
'ssh_port' => (int) $_POST['ssh_port'],
|
|
'ssh_user' => $_POST['ssh_user'],
|
|
'group_name' => $_POST['group_name'] ?? null,
|
|
'is_active' => (int) ($_POST['is_active'] ?? 1),
|
|
];
|
|
|
|
if (!empty($_POST['ssh_password'])) {
|
|
$data['ssh_password'] = $_POST['ssh_password'];
|
|
}
|
|
|
|
if (!empty($_POST['ssh_key'])) {
|
|
$data['ssh_key'] = $_POST['ssh_key'];
|
|
}
|
|
|
|
$this->serverModel->update($id, $data);
|
|
|
|
$this->auditService->log('server_updated', 'server', $id, [
|
|
'name' => $data['name'],
|
|
]);
|
|
|
|
Session::setFlash('success', 'Server updated successfully.');
|
|
$this->view->redirect("/servers/{$id}");
|
|
}
|
|
|
|
public function delete(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
$this->serverModel->delete($id);
|
|
|
|
$this->auditService->log('server_deleted', 'server', $id, [
|
|
'name' => $server['name'],
|
|
]);
|
|
|
|
Session::setFlash('success', 'Server deleted successfully.');
|
|
$this->view->redirect('/servers');
|
|
}
|
|
|
|
public function toggleActive(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$newState = $this->serverModel->toggleActive($id);
|
|
|
|
$this->auditService->log(
|
|
$newState ? 'server_activated' : 'server_deactivated',
|
|
'server',
|
|
$id,
|
|
['name' => $server['name']]
|
|
);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'is_active' => $newState,
|
|
'message' => $newState ? 'Server activated.' : 'Server deactivated.',
|
|
]);
|
|
}
|
|
|
|
public function testConnection(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$ssh = new SSHService();
|
|
$result = $ssh->testConnection($server);
|
|
|
|
$this->auditService->logCredentialAccess($id, 'SSH connection test');
|
|
|
|
$this->view->json([
|
|
'success' => $result,
|
|
'message' => $result ? 'Connection successful.' : 'Connection failed.',
|
|
]);
|
|
}
|
|
|
|
public function processes(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
$this->auditService->logCredentialAccess($id, 'View processes');
|
|
|
|
$result = $ssh->exec(
|
|
"ps aux --sort=-%cpu | awk 'NR>1{pid=\$2; user=\$1; cpu=\$3; mem=\$4; vsz=\$5; rss=\$6; tty=\$7; stat=\$8; start=\$9; time=\$10; cmd=substr(\$0, index(\$0,\$11)); print user\"|\"pid\"|\"cpu\"|\"mem\"|\"vsz\"|\"rss\"|\"tty\"|\"stat\"|\"start\"|\"time\"|\"cmd}' | head -50"
|
|
);
|
|
$ssh->disconnect();
|
|
|
|
$processList = [];
|
|
foreach (explode("\n", trim($result['output'])) as $line) {
|
|
$parts = explode('|', $line);
|
|
if (count($parts) >= 11) {
|
|
$processList[] = [
|
|
'user' => $parts[0],
|
|
'pid' => (int) $parts[1],
|
|
'cpu' => (float) $parts[2],
|
|
'mem' => (float) $parts[3],
|
|
'vsz' => (int) $parts[4],
|
|
'rss' => (int) $parts[5],
|
|
'tty' => $parts[6],
|
|
'stat' => $parts[7],
|
|
'start' => $parts[8],
|
|
'time' => $parts[9],
|
|
'command' => $parts[10],
|
|
];
|
|
}
|
|
}
|
|
|
|
$this->view->display('servers.processes', [
|
|
'title' => 'Processes - ' . $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'processList' => $processList,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Session::setFlash('error', 'Failed to get processes: ' . $e->getMessage());
|
|
$this->view->redirect("/servers/{$id}");
|
|
}
|
|
}
|
|
|
|
public function systemInfo(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
$this->auditService->logCredentialAccess($id, 'View system info');
|
|
|
|
$info = $ssh->getSystemInfo();
|
|
$ssh->disconnect();
|
|
|
|
$this->view->display('servers.system_info', [
|
|
'title' => 'System Info - ' . $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'systemInfo' => $info,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Session::setFlash('error', 'Failed to get system info: ' . $e->getMessage());
|
|
$this->view->redirect("/servers/{$id}");
|
|
}
|
|
}
|
|
|
|
public function logs(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
$logType = $_GET['type'] ?? 'syslog';
|
|
$lines = (int) ($_GET['lines'] ?? 100);
|
|
|
|
$logFiles = [
|
|
'syslog' => '/var/log/syslog',
|
|
'auth' => '/var/log/auth.log',
|
|
'messages' => '/var/log/messages',
|
|
'nginx' => '/var/log/nginx/error.log',
|
|
'apache' => '/var/log/apache2/error.log',
|
|
'mysql' => '/var/log/mysql/error.log',
|
|
'dmesg' => 'dmesg',
|
|
];
|
|
|
|
$logOutput = '';
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if ($ssh->connect($server)) {
|
|
$this->auditService->logCredentialAccess($id, 'View system logs');
|
|
|
|
if ($logType === 'dmesg') {
|
|
$result = $ssh->exec('dmesg | tail -n ' . $lines);
|
|
} else {
|
|
$logFile = $logFiles[$logType] ?? $logFiles['syslog'];
|
|
$result = $ssh->exec("tail -n {$lines} {$logFile} 2>/dev/null || echo 'Log file not found or not accessible'");
|
|
}
|
|
|
|
$logOutput = $result['output'];
|
|
$ssh->disconnect();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$logOutput = 'Error: ' . $e->getMessage();
|
|
}
|
|
|
|
$this->view->display('servers.logs', [
|
|
'title' => 'System Logs - ' . $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'logOutput' => $logOutput,
|
|
'logType' => $logType,
|
|
'lines' => $lines,
|
|
'logFiles' => array_keys($logFiles),
|
|
]);
|
|
}
|
|
|
|
public function reboot(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
$this->auditService->logServerAction($id, 'reboot', 'sudo reboot');
|
|
|
|
$ssh->exec('sudo reboot 2>&1 || echo "Reboot command sent (may require sudo)"');
|
|
$ssh->disconnect();
|
|
|
|
$this->auditService->log('server_rebooted', 'server', $id, ['name' => $server['name']]);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'message' => 'Reboot command sent.',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to send reboot command: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function shutdown(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
$this->auditService->logServerAction($id, 'shutdown', 'sudo shutdown -h now');
|
|
|
|
$ssh->exec('sudo shutdown -h now 2>&1 || echo "Shutdown command sent (may require sudo)"');
|
|
$ssh->disconnect();
|
|
|
|
$this->auditService->log('server_shutdown', 'server', $id, ['name' => $server['name']]);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'message' => 'Shutdown command sent.',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to send shutdown command: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function restartService(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$service = $_POST['service'] ?? '';
|
|
if (empty($service)) {
|
|
$this->view->json(['success' => false, 'message' => 'Service name is required']);
|
|
}
|
|
|
|
$allowedServices = ['nginx', 'apache2', 'mysql', 'mariadb', 'postgresql', 'redis', 'docker', 'sshd', 'cron'];
|
|
$service = preg_replace('/[^a-zA-Z0-9\-_]/', '', $service);
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
$this->auditService->logServerAction($id, 'service_restart', "sudo systemctl restart {$service}");
|
|
|
|
$result = $ssh->exec("sudo systemctl restart {$service} 2>&1");
|
|
$ssh->disconnect();
|
|
|
|
$this->auditService->log('service_restarted', 'server', $id, [
|
|
'name' => $server['name'],
|
|
'service' => $service,
|
|
]);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'message' => "Service '{$service}' restarted.",
|
|
'output' => $result['output'],
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to restart service: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function killProcess(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$pid = (int) ($_POST['pid'] ?? 0);
|
|
$signal = (int) ($_POST['signal'] ?? 15);
|
|
|
|
if ($pid <= 0) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid PID']);
|
|
}
|
|
|
|
$allowedSignals = [9, 15];
|
|
if (!in_array($signal, $allowedSignals, true)) {
|
|
$signal = 15;
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logServerAction($id, 'kill_process', "kill -{$signal} {$pid}");
|
|
$result = $ssh->exec("kill -{$signal} {$pid} 2>&1");
|
|
$ssh->disconnect();
|
|
|
|
$exitStatus = $result['exit_status'] ?? 0;
|
|
$success = $exitStatus === 0;
|
|
|
|
$this->view->json([
|
|
'success' => $success,
|
|
'message' => $success
|
|
? "Process {$pid} terminated with signal {$signal}."
|
|
: "Failed to kill process {$pid}: " . ($result['output'] ?: 'Unknown error'),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to kill process: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function nginx(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logCredentialAccess($id, 'View nginx config');
|
|
|
|
$installed = trim($ssh->exec('command -v nginx 2>/dev/null || which nginx 2>/dev/null || echo ""')['output'] ?? '');
|
|
$nginxFound = !empty($installed);
|
|
|
|
$data = [
|
|
'installed' => $nginxFound,
|
|
];
|
|
|
|
if ($nginxFound) {
|
|
$data['version'] = trim($ssh->exec('nginx -v 2>&1')['output'] ?? '');
|
|
|
|
$statusResult = $ssh->exec("systemctl is-active nginx 2>/dev/null || echo 'inactive'");
|
|
$data['status'] = trim($statusResult['output'] ?? 'inactive');
|
|
|
|
$enabledResult = $ssh->exec("systemctl is-enabled nginx 2>/dev/null || echo 'disabled'");
|
|
$data['enabled'] = trim($enabledResult['output'] ?? 'disabled');
|
|
|
|
$confResult = $ssh->exec('cat /etc/nginx/nginx.conf 2>/dev/null || echo "CONFIG_NOT_FOUND"');
|
|
$data['config'] = $confResult['output'] === 'CONFIG_NOT_FOUND' ? '' : $confResult['output'];
|
|
|
|
$sitesResult = $ssh->exec('ls -1 /etc/nginx/sites-enabled/ 2>/dev/null || echo ""');
|
|
$data['sites_enabled'] = array_filter(explode("\n", trim($sitesResult['output'])));
|
|
|
|
$availResult = $ssh->exec('ls -1 /etc/nginx/sites-available/ 2>/dev/null || echo ""');
|
|
$data['sites_available'] = array_filter(explode("\n", trim($availResult['output'])));
|
|
|
|
$testResult = $ssh->exec('sudo nginx -t 2>&1');
|
|
$data['config_test'] = trim($testResult['output']);
|
|
$data['config_valid'] = $testResult['exit_status'] === 0;
|
|
}
|
|
|
|
$ssh->disconnect();
|
|
|
|
$this->view->display('servers.nginx', [
|
|
'title' => 'Nginx - ' . $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'nginx' => $data,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Session::setFlash('error', 'Failed to get nginx info: ' . $e->getMessage());
|
|
$this->view->redirect("/servers/{$id}");
|
|
}
|
|
}
|
|
|
|
public function nginxGetFile(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$file = $_GET['file'] ?? '';
|
|
if (empty($file) || str_contains($file, '..')) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid file path']);
|
|
}
|
|
|
|
$allowedPaths = ['/etc/nginx/nginx.conf', '/etc/nginx/sites-available/', '/etc/nginx/sites-enabled/'];
|
|
$fullPath = str_starts_with($file, '/etc/nginx/') ? $file : '/etc/nginx/sites-available/' . basename($file);
|
|
|
|
$valid = false;
|
|
foreach ($allowedPaths as $path) {
|
|
if (str_starts_with($fullPath, $path)) {
|
|
$valid = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$valid) {
|
|
$this->view->json(['success' => false, 'message' => 'Access denied']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$result = $ssh->exec("cat " . escapeshellarg($fullPath) . " 2>/dev/null || echo 'FILE_NOT_FOUND'");
|
|
$ssh->disconnect();
|
|
|
|
if (trim($result['output']) === 'FILE_NOT_FOUND') {
|
|
$this->view->json(['success' => false, 'message' => 'File not found']);
|
|
}
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'path' => $fullPath,
|
|
'content' => $result['output'],
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json(['success' => false, 'message' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
public function nginxSaveFile(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$file = $_POST['file'] ?? '';
|
|
$content = $_POST['content'] ?? '';
|
|
|
|
if (empty($file) || str_contains($file, '..')) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid file path']);
|
|
}
|
|
|
|
$allowedPaths = ['/etc/nginx/nginx.conf', '/etc/nginx/sites-available/'];
|
|
$fullPath = str_starts_with($file, '/etc/nginx/') ? $file : '/etc/nginx/sites-available/' . basename($file);
|
|
|
|
$valid = false;
|
|
foreach ($allowedPaths as $path) {
|
|
if (str_starts_with($fullPath, $path)) {
|
|
$valid = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$valid) {
|
|
$this->view->json(['success' => false, 'message' => 'Access denied']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$b64 = base64_encode($content);
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo tee " . escapeshellarg($fullPath) . " > /dev/null 2>&1; echo EXIT:\$?");
|
|
$ssh->disconnect();
|
|
|
|
$this->auditService->logServerAction($id, 'nginx_save_config', $fullPath);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'message' => 'File saved successfully.',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to save file: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function nginxDeleteSite(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$site = $_POST['site'] ?? '';
|
|
if (empty($site) || str_contains($site, '..') || str_contains($site, '/')) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid site name']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logServerAction($id, 'nginx_delete_site', $site);
|
|
|
|
$result = $ssh->exec("sudo /bin/rm /etc/nginx/sites-enabled/" . escapeshellarg($site) . " /etc/nginx/sites-available/" . escapeshellarg($site) . " 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode("\n", $result['output'] ?? 'EXIT:-1')[0] ?? '-1');
|
|
|
|
$ssh->disconnect();
|
|
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "Site '{$site}' deleted." : "Failed to delete site '{$site}'.",
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to delete site: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function nginxCreateSite(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$siteName = trim($_POST['site_name'] ?? '');
|
|
$siteRoot = trim($_POST['site_root'] ?? '');
|
|
$serverName = trim($_POST['server_name'] ?? '');
|
|
$index = trim($_POST['index'] ?? 'index.html index.htm index.php');
|
|
$enablePhp = !empty($_POST['enable_php']);
|
|
$enableSite = !empty($_POST['enable_site']);
|
|
|
|
if (empty($siteName) || empty($siteRoot)) {
|
|
$this->view->json(['success' => false, 'message' => 'Site name and root directory are required']);
|
|
}
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9._-]+$/', $siteName)) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid site name. Use only letters, numbers, dots, hyphens.']);
|
|
}
|
|
|
|
if (str_starts_with($siteRoot, '/etc/nginx') || str_contains($siteRoot, '..')) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid root directory']);
|
|
}
|
|
|
|
if (empty($serverName)) {
|
|
$serverName = $siteName;
|
|
}
|
|
|
|
$config = "server {\n";
|
|
$config .= " listen 80;\n";
|
|
$config .= " listen [::]:80;\n\n";
|
|
$config .= " root {$siteRoot};\n";
|
|
$config .= " index {$index};\n\n";
|
|
$config .= " server_name {$serverName};\n\n";
|
|
$config .= " location / {\n";
|
|
$config .= " try_files \$uri \$uri/ =404;\n";
|
|
$config .= " }\n";
|
|
|
|
if ($enablePhp) {
|
|
$config .= "\n location ~ \\.php\$ {\n";
|
|
$config .= " include snippets/fastcgi-php.conf;\n";
|
|
$config .= " fastcgi_pass unix:/var/run/php/php-fpm.sock;\n";
|
|
$config .= " }\n";
|
|
}
|
|
|
|
$config .= "}\n";
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$file = "/etc/nginx/sites-available/{$siteName}";
|
|
$b64 = base64_encode($config);
|
|
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo tee " . escapeshellarg($file) . " > /dev/null 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim($result['output'] ?? '-1');
|
|
|
|
if ($exitCode !== 0) {
|
|
throw new \RuntimeException('Failed to create site configuration file');
|
|
}
|
|
|
|
$this->auditService->logServerAction($id, 'nginx_create_site', $siteName);
|
|
|
|
if ($enableSite) {
|
|
$linkResult = $ssh->exec('sudo ln -s ' . escapeshellarg($file) . ' /etc/nginx/sites-enabled/ 2>&1; echo EXIT:$?');
|
|
}
|
|
|
|
$ssh->disconnect();
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'message' => "Site '{$siteName}' created successfully." . ($enableSite ? ' Enabled.' : ''),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to create site: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function nginxToggleSite(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$site = $_POST['site'] ?? '';
|
|
$enable = (bool) ($_POST['enable'] ?? false);
|
|
|
|
if (empty($site) || str_contains($site, '..') || str_contains($site, '/')) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid site name']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
if ($enable) {
|
|
$result = $ssh->exec('sudo ln -s /etc/nginx/sites-available/' . escapeshellarg($site) . ' /etc/nginx/sites-enabled/ 2>&1; echo EXIT:$?');
|
|
} else {
|
|
$result = $ssh->exec('sudo rm /etc/nginx/sites-enabled/' . escapeshellarg($site) . ' 2>&1; echo EXIT:$?');
|
|
}
|
|
$ssh->disconnect();
|
|
|
|
$action = $enable ? 'enabled' : 'disabled';
|
|
$this->auditService->logServerAction($id, "nginx_{$action}_site", $site);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'message' => "Site '{$site}' {$action}.",
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to toggle site: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function nginxAction(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$action = $_POST['action'] ?? '';
|
|
$allowed = ['restart', 'reload', 'test', 'start', 'stop'];
|
|
if (!in_array($action, $allowed, true)) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid action']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logServerAction($id, "nginx_{$action}", "sudo systemctl {$action} nginx");
|
|
|
|
if ($action === 'test') {
|
|
$result = $ssh->exec('sudo nginx -t 2>&1');
|
|
$valid = $result['exit_status'] === 0;
|
|
$this->view->json([
|
|
'success' => $valid,
|
|
'message' => $valid ? 'Configuration is valid.' : 'Configuration test failed.',
|
|
'output' => trim($result['output']),
|
|
]);
|
|
} else {
|
|
$result = $ssh->exec("sudo systemctl {$action} nginx 2>&1");
|
|
$success = $result['exit_status'] === 0;
|
|
$msg = $success
|
|
? "Nginx {$action}ed successfully."
|
|
: "Failed to {$action} nginx: " . ($result['output'] ?: 'Unknown error');
|
|
$this->view->json(['success' => $success, 'message' => $msg, 'output' => trim($result['output'])]);
|
|
}
|
|
|
|
$ssh->disconnect();
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed to execute nginx action: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function ssl(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logCredentialAccess($id, 'View SSL certificates');
|
|
|
|
$installed = trim($ssh->exec('command -v certbot 2>/dev/null || which certbot 2>/dev/null || echo ""')['output'] ?? '');
|
|
$certbotFound = !empty($installed);
|
|
|
|
$data = ['certbot_installed' => $certbotFound];
|
|
|
|
if ($certbotFound) {
|
|
$data['version'] = trim($ssh->exec('certbot --version 2>&1')['output'] ?? '');
|
|
|
|
$certsResult = $ssh->exec('sudo certbot certificates 2>&1');
|
|
$data['certificates_raw'] = trim($certsResult['output']);
|
|
|
|
$certs = [];
|
|
$lines = explode("\n", $certsResult['output']);
|
|
$currentCert = null;
|
|
foreach ($lines as $line) {
|
|
if (preg_match('/^ Certificate Name:\s+(.+)$/', $line, $m)) {
|
|
if ($currentCert) $certs[] = $currentCert;
|
|
$currentCert = ['name' => trim($m[1]), 'domains' => [], 'expiry' => ''];
|
|
} elseif ($currentCert && preg_match('/^\s+Domains:\s+(.+)$/', $line, $m)) {
|
|
$currentCert['domains'] = array_map('trim', explode(' ', trim($m[1])));
|
|
} elseif ($currentCert && preg_match('/^\s+Expiry Date:\s+(.+?)(?:\s+\(VALID|$)/', $line, $m)) {
|
|
$currentCert['expiry'] = trim($m[1]);
|
|
} elseif ($currentCert && preg_match('/^\s+Certificate Path:\s+(.+)$/', $line, $m)) {
|
|
$currentCert['path'] = trim($m[1]);
|
|
} elseif ($currentCert && preg_match('/^\s+Key Type:\s+(.+)$/', $line, $m)) {
|
|
$currentCert['key_type'] = trim($m[1]);
|
|
}
|
|
}
|
|
if ($currentCert) $certs[] = $currentCert;
|
|
$data['certificates'] = $certs;
|
|
|
|
$renewalResult = $ssh->exec('certbot renew --dry-run 2>&1 | tail -5');
|
|
$data['renewal_status'] = trim($renewalResult['output']);
|
|
} else {
|
|
$osResult = $ssh->exec('(. /etc/os-release && echo $ID) 2>/dev/null || echo "unknown"');
|
|
$data['os'] = trim($osResult['output'] ?? 'unknown');
|
|
}
|
|
|
|
$ssh->disconnect();
|
|
|
|
$this->view->display('servers.ssl', [
|
|
'title' => 'SSL Certificates - ' . $server['name'] . ' - ServerManager',
|
|
'server' => $server,
|
|
'ssl' => $data,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Session::setFlash('error', 'Failed to get SSL info: ' . $e->getMessage());
|
|
$this->view->redirect("/servers/{$id}");
|
|
}
|
|
}
|
|
|
|
public function sslAction(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$action = $_POST['action'] ?? '';
|
|
$allowed = ['install', 'request', 'renew', 'delete'];
|
|
if (!in_array($action, $allowed, true)) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid action']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logServerAction($id, "certbot_{$action}", $action);
|
|
|
|
if ($action === 'install') {
|
|
$osResult = $ssh->exec('(. /etc/os-release && echo $ID) 2>/dev/null || echo "unknown"');
|
|
$os = trim($osResult['output'] ?? 'unknown');
|
|
|
|
if ($os === 'ubuntu' || $os === 'debian') {
|
|
$result = $ssh->exec('sudo apt-get update -qq 2>&1 && sudo apt-get install -y -qq certbot python3-certbot-nginx 2>&1; echo EXIT:$?');
|
|
} elseif ($os === 'centos' || $os === 'fedora' || $os === 'rhel' || str_contains($os, 'rocky') || str_contains($os, 'almalinux')) {
|
|
$result = $ssh->exec('sudo dnf install -y -q certbot python3-certbot-nginx 2>&1; echo EXIT:$?');
|
|
} else {
|
|
$result = $ssh->exec('sudo apt-get install -y -qq certbot 2>&1 || sudo yum install -y -q certbot 2>&1; echo EXIT:$?');
|
|
}
|
|
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? 'Certbot installed successfully.' : 'Failed to install certbot. Try installing manually.',
|
|
'output' => trim($result['output']),
|
|
]);
|
|
} elseif ($action === 'request') {
|
|
$domains = trim($_POST['domains'] ?? '');
|
|
$email = trim($_POST['email'] ?? '');
|
|
|
|
if (empty($domains)) {
|
|
$this->view->json(['success' => false, 'message' => 'Domain is required']);
|
|
}
|
|
|
|
$domainList = preg_replace('/\s+/', ',', $domains);
|
|
$emailArg = !empty($email) ? "--email {$email}" : '--register-unsafely-without-email';
|
|
$cmd = "sudo certbot --nginx -d {$domainList} {$emailArg} --non-interactive --agree-tos 2>&1; echo EXIT:\$?";
|
|
$result = $ssh->exec($cmd);
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? 'Certificate obtained successfully.' : 'Failed to obtain certificate.',
|
|
'output' => trim($result['output']),
|
|
]);
|
|
} elseif ($action === 'renew') {
|
|
$result = $ssh->exec('sudo certbot renew --non-interactive 2>&1; echo EXIT:$?');
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? 'Certificates renewed.' : 'Renewal attempted with some issues.',
|
|
'output' => trim($result['output']),
|
|
]);
|
|
} elseif ($action === 'delete') {
|
|
$certName = trim($_POST['cert_name'] ?? '');
|
|
if (empty($certName)) {
|
|
$this->view->json(['success' => false, 'message' => 'Certificate name is required']);
|
|
}
|
|
$result = $ssh->exec("sudo certbot delete --non-interactive --cert-name " . escapeshellarg($certName) . " 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "Certificate '{$certName}' deleted." : 'Failed to delete certificate.',
|
|
'output' => trim($result['output']),
|
|
]);
|
|
} else {
|
|
$ssh->disconnect();
|
|
$this->view->json(['success' => false, 'message' => 'Unknown action']);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function database(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->error(404);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logCredentialAccess($id, 'View database manager');
|
|
|
|
$mysqlFound = trim($ssh->exec('command -v mysql 2>/dev/null || which mysql 2>/dev/null || echo ""')['output'] ?? '');
|
|
$mariadbFound = trim($ssh->exec('command -v mariadb 2>/dev/null || which mariadb 2>/dev/null || echo ""')['output'] ?? '');
|
|
$hasMysql = !empty($mysqlFound);
|
|
$hasMariadb = !empty($mariadbFound);
|
|
|
|
$dbType = '';
|
|
$version = '';
|
|
$dbStatus = '';
|
|
|
|
if ($hasMariadb) {
|
|
$dbType = 'MariaDB';
|
|
$version = trim($ssh->exec('mariadb --version 2>&1')['output'] ?? '');
|
|
$dbStatus = trim($ssh->exec("systemctl is-active mariadb 2>/dev/null || echo 'inactive'")['output'] ?? 'inactive');
|
|
} elseif ($hasMysql) {
|
|
$versionResult = $ssh->exec('mysql --version 2>&1');
|
|
$version = trim($versionResult['output'] ?? '');
|
|
if (stripos($version, 'mariadb') !== false) {
|
|
$dbType = 'MariaDB';
|
|
$dbStatus = trim($ssh->exec("systemctl is-active mariadb 2>/dev/null || echo 'inactive'")['output'] ?? 'inactive');
|
|
} else {
|
|
$dbType = 'MySQL';
|
|
$dbStatus = trim($ssh->exec("systemctl is-active mysql 2>/dev/null || echo 'inactive'")['output'] ?? 'inactive');
|
|
}
|
|
$hasMysql = true;
|
|
}
|
|
|
|
$data = [
|
|
'installed' => $hasMysql || $hasMariadb,
|
|
'db_type' => $dbType,
|
|
'version' => $version,
|
|
'status' => $dbStatus,
|
|
];
|
|
|
|
if ($data['installed']) {
|
|
$mysqlBin = $dbType === 'MariaDB' ? 'mariadb' : 'mysql';
|
|
|
|
$dbsResult = $ssh->exec("sudo {$mysqlBin} --batch -e \"SELECT TABLE_SCHEMA AS name, ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 1) AS size_mb FROM information_schema.TABLES GROUP BY TABLE_SCHEMA ORDER BY size_mb DESC\" 2>/dev/null | tail -n +2 || echo 'ACCESS_DENIED'");
|
|
$dbsOutput = trim($dbsResult['output'] ?? '');
|
|
|
|
$databases = [];
|
|
if ($dbsOutput && $dbsOutput !== 'NO_ACCESS') {
|
|
foreach (explode("\n", $dbsOutput) as $line) {
|
|
$parts = preg_split('/\s+/', trim($line), 2);
|
|
if (count($parts) === 2) {
|
|
$databases[] = ['name' => $parts[0], 'size_mb' => (float) $parts[1]];
|
|
}
|
|
}
|
|
}
|
|
$data['databases'] = $databases;
|
|
|
|
$mysqlBin = $dbType === 'MariaDB' ? 'mariadb' : 'mysql';
|
|
$sizeResult = $ssh->exec("sudo {$mysqlBin} -e \"SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 1) AS total_mb FROM information_schema.TABLES\" 2>/dev/null | tail -1");
|
|
$data['total_size_mb'] = (float) trim($sizeResult['output'] ?? '0');
|
|
|
|
$processResult = $ssh->exec("sudo {$mysqlBin} -e \"SHOW PROCESSLIST\" 2>/dev/null | head -20");
|
|
$processes = [];
|
|
$lines = explode("\n", trim($processResult['output'] ?? ''));
|
|
$header = true;
|
|
foreach ($lines as $line) {
|
|
if ($header) { $header = false; continue; }
|
|
$parts = preg_split('/\s+\|\s+/', trim($line, '| '));
|
|
if (count($parts) >= 5) {
|
|
$processes[] = [
|
|
'id' => $parts[0],
|
|
'user' => $parts[1],
|
|
'host' => $parts[2],
|
|
'db' => $parts[3] ?? '',
|
|
'command' => $parts[4] ?? '',
|
|
'time' => $parts[5] ?? '0',
|
|
'state' => $parts[6] ?? '',
|
|
'info' => $parts[7] ?? '',
|
|
];
|
|
}
|
|
}
|
|
$data['processes'] = array_slice($processes, 0, 20);
|
|
}
|
|
|
|
$ssh->disconnect();
|
|
|
|
$this->view->display('servers.database', [
|
|
'title' => "{$dbType} Manager - {$server['name']} - ServerManager",
|
|
'server' => $server,
|
|
'db' => $data,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Session::setFlash('error', 'Failed to get database info: ' . $e->getMessage());
|
|
$this->view->redirect("/servers/{$id}");
|
|
}
|
|
}
|
|
|
|
public function databaseAction(int $id): void
|
|
{
|
|
$server = $this->serverModel->findById($id);
|
|
|
|
if (!$server) {
|
|
$this->view->json(['success' => false, 'message' => 'Server not found'], 404);
|
|
}
|
|
|
|
$action = $_POST['action'] ?? '';
|
|
$allowed = ['query', 'tables', 'structure', 'browse', 'users', 'create_user', 'grant', 'revoke', 'drop_user', 'change_pass'];
|
|
if (!in_array($action, $allowed, true)) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid action']);
|
|
}
|
|
|
|
try {
|
|
$ssh = new SSHService();
|
|
if (!$ssh->connect($server)) {
|
|
throw new \RuntimeException('Could not establish SSH connection');
|
|
}
|
|
|
|
$this->auditService->logServerAction($id, "db_{$action}", $action);
|
|
|
|
$mysqlBinResult = $ssh->exec('command -v mariadb 2>/dev/null || echo mysql');
|
|
$mysqlBin = trim($mysqlBinResult['output'] ?? 'mariadb');
|
|
$db = trim($_POST['db'] ?? '');
|
|
$table = trim($_POST['table'] ?? '');
|
|
|
|
if ($action === 'tables') {
|
|
if (empty($db)) {
|
|
$this->view->json(['success' => false, 'message' => 'Database is required']);
|
|
}
|
|
$escapedDb = str_replace("'", "'\\''", $db);
|
|
$result = $ssh->exec("sudo {$mysqlBin} --batch -e 'USE `{$escapedDb}`; SHOW TABLE STATUS' 2>&1");
|
|
$lines = explode("\n", trim($result['output'] ?? ''));
|
|
$tables = [];
|
|
$header = true;
|
|
foreach ($lines as $line) {
|
|
if ($header) { $header = false; continue; }
|
|
$parts = explode("\t", $line);
|
|
if (count($parts) >= 5) {
|
|
$tables[] = [
|
|
'name' => $parts[0] ?? '',
|
|
'engine' => $parts[1] ?? '',
|
|
'rows' => (int) ($parts[4] ?? 0),
|
|
'size' => (int) (($parts[6] ?? 0) + ($parts[8] ?? 0)),
|
|
'collation' => $parts[14] ?? '',
|
|
];
|
|
}
|
|
}
|
|
$ssh->disconnect();
|
|
$this->view->json(['success' => true, 'tables' => $tables]);
|
|
} elseif ($action === 'structure') {
|
|
if (empty($db) || empty($table)) {
|
|
$this->view->json(['success' => false, 'message' => 'Database and table are required']);
|
|
}
|
|
$escapedDb = str_replace("'", "'\\''", $db);
|
|
$escapedTable = str_replace("'", "'\\''", $table);
|
|
$result = $ssh->exec("sudo {$mysqlBin} --batch -e 'SHOW FULL COLUMNS FROM `{$escapedDb}`.`{$escapedTable}`' 2>&1");
|
|
$lines = explode("\n", trim($result['output'] ?? ''));
|
|
$columns = [];
|
|
$header = true;
|
|
foreach ($lines as $line) {
|
|
if ($header) { $header = false; continue; }
|
|
if (str_starts_with($line, '---')) continue;
|
|
$parts = explode("\t", $line);
|
|
if (count($parts) >= 6) {
|
|
$columns[] = [
|
|
'field' => $parts[0] ?? '',
|
|
'type' => $parts[1] ?? '',
|
|
'collation' => $parts[2] ?? '',
|
|
'null' => $parts[3] ?? '',
|
|
'key' => $parts[4] ?? '',
|
|
'default' => $parts[5] ?? '',
|
|
'extra' => $parts[6] ?? '',
|
|
];
|
|
}
|
|
}
|
|
$ssh->disconnect();
|
|
$this->view->json(['success' => true, 'columns' => $columns]);
|
|
} elseif ($action === 'browse') {
|
|
if (empty($db) || empty($table)) {
|
|
$this->view->json(['success' => false, 'message' => 'Database and table are required']);
|
|
}
|
|
$page = max(1, (int) ($_POST['page'] ?? 1));
|
|
$limit = 50;
|
|
$offset = ($page - 1) * $limit;
|
|
$escapedDb = str_replace("'", "'\\''", $db);
|
|
$escapedTable = str_replace("'", "'\\''", $table);
|
|
|
|
$colsResult = $ssh->exec("sudo {$mysqlBin} --batch -e 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=\"{$escapedDb}\" AND TABLE_NAME=\"{$escapedTable}\" ORDER BY ORDINAL_POSITION' 2>&1 | tail -n +2");
|
|
$columns = array_map('trim', array_filter(explode("\n", trim($colsResult['output'] ?? ''))));
|
|
|
|
$where = '';
|
|
$conditions = [];
|
|
foreach ($columns as $col) {
|
|
$val = trim($_POST[$col] ?? '');
|
|
if ($val !== '') {
|
|
$escapedVal = str_replace(["\\", "'"], ["\\\\", "'\\''"], $val);
|
|
$conditions[] = "`{$escapedTable}`.`{$col}` LIKE '%{$escapedVal}%'";
|
|
}
|
|
}
|
|
if (!empty($conditions)) {
|
|
$where = ' WHERE ' . implode(' AND ', $conditions);
|
|
}
|
|
|
|
$escapedWhere = str_replace("'", "'\\''", $where);
|
|
$countResult = $ssh->exec("sudo {$mysqlBin} --batch -e 'SELECT COUNT(*) AS cnt FROM `{$escapedDb}`.`{$escapedTable}`{$escapedWhere}' 2>&1 | tail -1");
|
|
$totalRows = (int) trim($countResult['output'] ?? '0');
|
|
|
|
$data = [];
|
|
if ($totalRows > 0) {
|
|
$result = $ssh->exec("sudo {$mysqlBin} --batch -e 'SELECT * FROM `{$escapedDb}`.`{$escapedTable}`{$escapedWhere} LIMIT {$limit} OFFSET {$offset}' 2>&1 | tail -n +2");
|
|
if (trim($result['output'] ?? '')) {
|
|
foreach (explode("\n", trim($result['output'])) as $line) {
|
|
$data[] = explode("\t", $line);
|
|
}
|
|
}
|
|
}
|
|
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => true,
|
|
'columns' => $columns,
|
|
'rows' => $data,
|
|
'total' => $totalRows,
|
|
'page' => $page,
|
|
'per_page' => $limit,
|
|
]);
|
|
} elseif ($action === 'users') {
|
|
$userResult = $ssh->exec("sudo {$mysqlBin} --batch -e \"SELECT User, Host, password_expired FROM mysql.user ORDER BY User\"");
|
|
$users = [];
|
|
$userLines = explode("\n", trim($userResult['output'] ?? ''));
|
|
array_shift($userLines);
|
|
foreach ($userLines as $line) {
|
|
$parts = explode("\t", $line);
|
|
if (count($parts) >= 2 && $parts[0] !== '' && $parts[0] !== 'User') {
|
|
$users[] = ['user' => $parts[0], 'host' => $parts[1], 'expired' => $parts[2] ?? '', 'locked' => ''];
|
|
}
|
|
}
|
|
$grantsResult = $ssh->exec("sudo {$mysqlBin} --batch -e \"SELECT User, Host, Db FROM mysql.db ORDER BY User, Db\"");
|
|
$grants = [];
|
|
$grantLines = explode("\n", trim($grantsResult['output'] ?? ''));
|
|
array_shift($grantLines);
|
|
foreach ($grantLines as $line) {
|
|
$parts = explode("\t", $line);
|
|
if (count($parts) >= 3 && $parts[0] !== '') {
|
|
$grants[] = ['user' => $parts[0], 'host' => $parts[1], 'db' => $parts[2], 'table_priv' => '', 'column_priv' => ''];
|
|
}
|
|
}
|
|
$ssh->disconnect();
|
|
$this->view->json(['success' => true, 'users' => $users, 'grants' => $grants]);
|
|
} elseif ($action === 'create_user') {
|
|
$newUser = trim($_POST['new_user'] ?? '');
|
|
$newPass = trim($_POST['new_pass'] ?? '');
|
|
$newHost = trim($_POST['new_host'] ?? 'localhost');
|
|
if (empty($newUser) || empty($newPass)) {
|
|
$this->view->json(['success' => false, 'message' => 'Username and password are required']);
|
|
}
|
|
if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $newUser)) {
|
|
$this->view->json(['success' => false, 'message' => 'Invalid username']);
|
|
}
|
|
$sqlCreate = "CREATE USER '{$newUser}'@'{$newHost}' IDENTIFIED BY '{$newPass}'";
|
|
$b64 = base64_encode($sqlCreate);
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo {$mysqlBin} 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "User '{$newUser}' created." : 'Failed to create user.',
|
|
'output' => trim($result['output']),
|
|
]);
|
|
} elseif ($action === 'grant') {
|
|
$grantUser = trim($_POST['grant_user'] ?? '');
|
|
$grantDb = trim($_POST['grant_db'] ?? '');
|
|
$grantHost = trim($_POST['grant_host'] ?? 'localhost');
|
|
$grantPrivs = trim($_POST['grant_privs'] ?? 'ALL PRIVILEGES');
|
|
if (empty($grantUser) || empty($grantDb)) {
|
|
$this->view->json(['success' => false, 'message' => 'User and database are required']);
|
|
}
|
|
$sqlGrant = "GRANT {$grantPrivs} ON {$grantDb}.* TO '{$grantUser}'@'{$grantHost}'; FLUSH PRIVILEGES";
|
|
$b64 = base64_encode($sqlGrant);
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo {$mysqlBin} 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "Privileges granted to '{$grantUser}' on {$grantDb}." : 'Failed to grant privileges.',
|
|
]);
|
|
} elseif ($action === 'revoke') {
|
|
$revokeUser = trim($_POST['revoke_user'] ?? '');
|
|
$revokeDb = trim($_POST['revoke_db'] ?? '');
|
|
$revokeHost = trim($_POST['revoke_host'] ?? 'localhost');
|
|
if (empty($revokeUser) || empty($revokeDb)) {
|
|
$this->view->json(['success' => false, 'message' => 'User and database are required']);
|
|
}
|
|
$sqlRevoke = "REVOKE ALL PRIVILEGES ON {$revokeDb}.* FROM '{$revokeUser}'@'{$revokeHost}'; FLUSH PRIVILEGES";
|
|
$b64 = base64_encode($sqlRevoke);
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo {$mysqlBin} 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "Privileges revoked from '{$revokeUser}' on {$revokeDb}." : 'Failed to revoke privileges.',
|
|
]);
|
|
} elseif ($action === 'query') {
|
|
$query = trim($_POST['query'] ?? '');
|
|
if (empty($query)) {
|
|
$this->view->json(['success' => false, 'message' => 'Query is required']);
|
|
}
|
|
$upper = strtoupper(substr(trim($query), 0, 6));
|
|
$allowedPrefixes = ['SELECT', 'SHOW', 'DESC', 'EXPLAI'];
|
|
$blocked = true;
|
|
foreach ($allowedPrefixes as $prefix) {
|
|
if (str_starts_with($upper, $prefix)) {
|
|
$blocked = false;
|
|
break;
|
|
}
|
|
}
|
|
if ($blocked) {
|
|
$this->view->json(['success' => false, 'message' => 'Only SELECT/SHOW/DESCRIBE/EXPLAIN queries are allowed']);
|
|
}
|
|
$escaped = str_replace("'", "'\\''", $query);
|
|
$result = $ssh->exec("sudo {$mysqlBin} -e '{$escaped}' 2>&1");
|
|
$output = trim($result['output'] ?? '');
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => !empty($output),
|
|
'message' => 'Query executed.',
|
|
'output' => $output,
|
|
]);
|
|
} elseif ($action === 'drop_user') {
|
|
$targetUser = trim($_POST['target_user'] ?? '');
|
|
$targetHost = trim($_POST['target_host'] ?? 'localhost');
|
|
if (empty($targetUser)) {
|
|
$this->view->json(['success' => false, 'message' => 'Username required']);
|
|
}
|
|
$sql = "DROP USER '{$targetUser}'@'{$targetHost}'";
|
|
$b64 = base64_encode($sql);
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo {$mysqlBin} 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "User '{$targetUser}' deleted." : 'Failed to delete user.',
|
|
]);
|
|
} elseif ($action === 'change_pass') {
|
|
$targetUser = trim($_POST['target_user'] ?? '');
|
|
$targetHost = trim($_POST['target_host'] ?? 'localhost');
|
|
$newPass = trim($_POST['new_pass'] ?? '');
|
|
if (empty($targetUser) || empty($newPass)) {
|
|
$this->view->json(['success' => false, 'message' => 'Username and password required']);
|
|
}
|
|
$sql = "ALTER USER '{$targetUser}'@'{$targetHost}' IDENTIFIED BY '{$newPass}'";
|
|
$b64 = base64_encode($sql);
|
|
$result = $ssh->exec("echo {$b64} | base64 -d | sudo {$mysqlBin} 2>&1; echo EXIT:\$?");
|
|
$exitCode = (int) trim(explode('EXIT:', $result['output'] ?? 'EXIT:-1')[1] ?? '-1');
|
|
$ssh->disconnect();
|
|
$this->view->json([
|
|
'success' => $exitCode === 0,
|
|
'message' => $exitCode === 0 ? "Password changed for '{$targetUser}'." : 'Failed to change password.',
|
|
]);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->view->json([
|
|
'success' => false,
|
|
'message' => 'Failed: ' . $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function sidebarServers(): void
|
|
{
|
|
$servers = $this->serverModel->findAll();
|
|
$result = [];
|
|
foreach ($servers as $s) {
|
|
$result[] = [
|
|
'id' => $s['id'],
|
|
'name' => $s['name'],
|
|
'current_status' => $s['current_status'] ?? 'unknown',
|
|
'ip_address' => $s['ip_address'] ?? '',
|
|
];
|
|
}
|
|
$this->view->json(['success' => true, 'data' => $result]);
|
|
}
|
|
|
|
public function metrics(int $id): void
|
|
{
|
|
$monitoringService = new MonitoringService();
|
|
$metrics = $monitoringService->getLatestMetrics($id);
|
|
|
|
$this->view->json([
|
|
'success' => true,
|
|
'data' => $metrics,
|
|
]);
|
|
}
|
|
}
|