Plugins base (#613)

* wip

* wip

* cleanup

* notification channels

* phpstan

* services

* remove server types

* refactoring

* refactoring
This commit is contained in:
Saeed Vaziry
2025-06-14 14:35:18 +02:00
committed by GitHub
parent adc0653d15
commit 131b828807
311 changed files with 3976 additions and 2660 deletions

View File

@ -1,24 +0,0 @@
<?php
namespace App\SSH;
trait HasS3Storage
{
private function prepareS3Path(string $path, string $prefix = ''): string
{
$path = trim($path);
$path = ltrim($path, '/');
$path = preg_replace('/[^a-zA-Z0-9\-_\.\/]/', '_', $path);
$path = preg_replace('/\/+/', '/', (string) $path);
if ($prefix !== '' && $prefix !== '0') {
return trim($prefix, '/').'/'.$path;
}
if ($path === null) {
throw new \Exception('Invalid S3 path');
}
return $path;
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\SSH\Composer;
namespace App\SSH\OS;
use App\Exceptions\SSHError;
use App\Models\Site;

View File

@ -1,6 +1,6 @@
<?php
namespace App\SSH\Cron;
namespace App\SSH\OS;
use App\Exceptions\SSHError;
use App\Models\Server;

View File

@ -1,6 +1,6 @@
<?php
namespace App\SSH\Git;
namespace App\SSH\OS;
use App\Exceptions\SSHError;
use App\Models\Site;

View File

@ -225,7 +225,7 @@ public function tail(string $path, int $lines): string
public function runScript(string $path, string $script, ?ServerLog $serverLog, ?string $user = null, ?array $variables = []): ServerLog
{
$ssh = $this->server->ssh($user);
if ($serverLog instanceof \App\Models\ServerLog) {
if ($serverLog instanceof ServerLog) {
$ssh->setLog($serverLog);
}
$command = '';

View File

@ -1,6 +1,6 @@
<?php
namespace App\SSH\Systemd;
namespace App\SSH\OS;
use App\Exceptions\SSHError;
use App\Models\Server;

View File

@ -1,24 +0,0 @@
<?php
namespace App\SSH\PHPMyAdmin;
use App\Exceptions\SSHError;
use App\Models\Site;
class PHPMyAdmin
{
/**
* @throws SSHError
*/
public function install(Site $site): void
{
$site->server->ssh($site->user)->exec(
view('ssh.phpmyadmin.install', [
'version' => $site->type_data['version'],
'path' => $site->path,
]),
'install-phpmyadmin',
$site->id
);
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace App\SSH\Services;
use App\Models\Service;
abstract class AbstractService implements ServiceInterface
{
public function __construct(protected Service $service) {}
public function creationRules(array $input): array
{
return [];
}
public function creationData(array $input): array
{
return [];
}
public function deletionRules(): array
{
return [];
}
public function data(): array
{
return [];
}
public function install(): void
{
//
}
public function uninstall(): void
{
//
}
}

View File

@ -1,366 +0,0 @@
<?php
namespace App\SSH\Services\Database;
use App\Actions\Database\SyncDatabases;
use App\Enums\BackupStatus;
use App\Exceptions\ServiceInstallationFailed;
use App\Exceptions\SSHError;
use App\Models\BackupFile;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractDatabase extends AbstractService implements Database
{
/**
* @var array<string>
*/
protected array $systemDbs = [];
/**
* @var array<string>
*/
protected array $systemUsers = [];
protected string $defaultCharset;
protected string $separator = "\t";
protected int $headerLines = 1;
protected bool $removeLastRow = false;
/**
* @phpstan-return view-string
*/
protected function getScriptView(string $script): string
{
/** @phpstan-ignore-next-line */
return 'ssh.services.database.'.$this->service->name.'.'.$script;
}
public function creationRules(array $input): array
{
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail): void {
$databaseExists = $this->service->server->database();
if ($databaseExists) {
$fail('You already have a database service on the server.');
}
},
],
];
}
/**
* @throws ServiceInstallationFailed
* @throws SSHError
*/
public function install(): void
{
$version = str_replace('.', '', $this->service->version);
$command = view($this->getScriptView('install-'.$version));
$this->service->server->ssh()->exec($command, 'install-'.$this->service->name.'-'.$version);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
/** @TODO implement post-install for services and move it there */
app(SyncDatabases::class)->sync($this->service->server);
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail): void {
$hasDatabase = $this->service->server->databases()->exists();
if ($hasDatabase) {
$fail('You have database(s) on the server.');
}
$hasDatabaseUser = $this->service->server->databaseUsers()->exists();
if ($hasDatabaseUser) {
$fail('You have database user(s) on the server.');
}
$hasRunningBackup = $this->service->server->backups()
->where('status', BackupStatus::RUNNING)
->exists();
if ($hasRunningBackup) {
$fail('You have database backup(s) on the server.');
}
},
],
];
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$version = $this->service->version;
$command = view($this->getScriptView('uninstall'));
$this->service->server->ssh()->exec($command, 'uninstall-'.$this->service->name.'-'.$version);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function create(string $name, string $charset, string $collation): void
{
$this->service->server->ssh()->exec(
view($this->getScriptView('create'), [
'name' => $name,
'charset' => $charset,
'collation' => $collation,
]),
'create-database'
);
}
/**
* @throws SSHError
*/
public function delete(string $name): void
{
$this->service->server->ssh()->exec(
view($this->getScriptView('delete'), [
'name' => $name,
]),
'delete-database'
);
}
/**
* @throws SSHError
*/
public function createUser(string $username, string $password, string $host): void
{
$this->service->server->ssh()->exec(
view($this->getScriptView('create-user'), [
'username' => $username,
'password' => $password,
'host' => $host,
]),
'create-user'
);
}
/**
* @throws SSHError
*/
public function deleteUser(string $username, string $host): void
{
$this->service->server->ssh()->exec(
view($this->getScriptView('delete-user'), [
'username' => $username,
'host' => $host,
]),
'delete-user'
);
}
/**
* @throws SSHError
*/
public function link(string $username, string $host, array $databases): void
{
$ssh = $this->service->server->ssh();
$version = $this->service->version;
foreach ($databases as $database) {
$ssh->exec(
view($this->getScriptView('link'), [
'username' => $username,
'host' => $host,
'database' => $database,
'version' => $version,
]),
'link-user-to-database'
);
}
}
/**
* @throws SSHError
*/
public function unlink(string $username, string $host): void
{
$version = $this->service->version;
$this->service->server->ssh()->exec(
view($this->getScriptView('unlink'), [
'username' => $username,
'host' => $host,
'version' => $version,
]),
'unlink-user-from-databases'
);
}
/**
* @throws SSHError
*/
public function runBackup(BackupFile $backupFile): void
{
// backup
$this->service->server->ssh()->exec(
view($this->getScriptView('backup'), [
'file' => $backupFile->name,
'database' => $backupFile->backup->database->name,
]),
'backup-database'
);
// upload to storage
$upload = $backupFile->backup->storage->provider()->ssh($this->service->server)->upload(
$backupFile->tempPath(),
$backupFile->path(),
);
// cleanup
$this->service->server->ssh()->exec('rm '.$backupFile->tempPath());
$backupFile->size = $upload['size'];
$backupFile->save();
}
/**
* @throws SSHError
*/
public function restoreBackup(BackupFile $backupFile, string $database): void
{
// download
$backupFile->backup->storage->provider()->ssh($this->service->server)->download(
$backupFile->path(),
$backupFile->tempPath(),
);
$this->service->server->ssh()->exec(
view($this->getScriptView('restore'), [
'database' => $database,
'file' => rtrim($backupFile->tempPath(), '.zip'),
]),
'restore-database'
);
}
/**
* @throws SSHError
*/
public function getCharsets(): array
{
$data = $this->service->server->ssh()->exec(
view($this->getScriptView('get-charsets')),
'get-database-charsets'
);
$charsets = $this->tableToArray($data);
$results = [];
$charsetCollations = [];
foreach ($charsets as $key => $charset) {
if (empty($charsetCollations[$charset[1]])) {
$charsetCollations[$charset[1]] = [];
}
$charsetCollations[$charset[1]][] = $charset[0];
if ($charset[3] === 'Yes') {
$results[$charset[1]] = [
'default' => $charset[0],
'list' => [],
];
continue;
}
if ($key == count($charsets) - 1) {
$results[$charset[1]] = [
'default' => null,
'list' => [],
];
}
}
foreach (array_keys($results) as $charset) {
$results[$charset]['list'] = $charsetCollations[$charset];
}
ksort($results);
return [
'charsets' => $results,
'defaultCharset' => $this->defaultCharset,
];
}
/**
* @throws SSHError
*/
public function getDatabases(): array
{
$data = $this->service->server->ssh()->exec(
view($this->getScriptView('get-db-list')),
'get-db-list'
);
$databases = $this->tableToArray($data);
return array_values(array_filter($databases, fn ($database): bool => ! in_array($database[0], $this->systemDbs)));
}
/**
* @throws SSHError
*/
public function getUsers(): array
{
$data = $this->service->server->ssh()->exec(
view($this->getScriptView('get-users-list')),
'get-users-list'
);
$users = $this->tableToArray($data);
$users = array_values(array_filter($users, fn ($users): bool => ! in_array($users[0], $this->systemUsers)));
foreach ($users as $key => $user) {
$databases = explode(',', $user[2]);
$databases = array_values(array_filter($databases, fn ($database): bool => ! in_array($database, $this->systemDbs)));
$users[$key][2] = implode(',', $databases);
}
return $users;
}
/**
* @return array<array<string>>
*/
protected function tableToArray(string $data, bool $keepHeader = false): array
{
$lines = explode("\n", trim($data));
if (! $keepHeader) {
for ($i = 0; $i < $this->headerLines; $i++) {
array_shift($lines);
}
}
if ($this->removeLastRow) {
array_pop($lines);
}
$rows = [];
foreach ($lines as $line) {
$separator = $this->separator === '' || $this->separator === '0' ? "\t" : $this->separator;
$row = explode($separator, $line);
$row = array_map('trim', $row);
$rows[] = $row;
}
return $rows;
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\SSH\Services\Database;
use App\Models\BackupFile;
use App\SSH\Services\ServiceInterface;
interface Database extends ServiceInterface
{
public function create(string $name, string $charset, string $collation): void;
public function delete(string $name): void;
public function createUser(string $username, string $password, string $host): void;
public function deleteUser(string $username, string $host): void;
/**
* @param array<string> $databases
*/
public function link(string $username, string $host, array $databases): void;
public function unlink(string $username, string $host): void;
public function runBackup(BackupFile $backupFile): void;
public function restoreBackup(BackupFile $backupFile, string $database): void;
/**
* @return array<string, mixed>
*/
public function getCharsets(): array;
/**
* @return array<int, array<string>>
*/
public function getDatabases(): array;
/**
* @return array<int, array<string>>
*/
public function getUsers(): array;
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\SSH\Services\Database;
class Mariadb extends AbstractDatabase
{
protected array $systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
protected array $systemUsers = [
'root',
'mysql',
'mariadb.sys',
];
protected string $defaultCharset = 'utf8mb3';
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\SSH\Services\Database;
class Mysql extends AbstractDatabase
{
protected array $systemDbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
protected array $systemUsers = [
'root',
'mysql.session',
'mysql.sys',
'mysql.infoschema',
];
protected string $defaultCharset = 'utf8mb3';
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\SSH\Services\Database;
class Postgresql extends AbstractDatabase
{
protected array $systemDbs = ['template0', 'template1', 'postgres'];
/**
* @var string[]
*/
protected array $systemUsers = ['postgres'];
protected string $defaultCharset = 'UTF8';
protected int $headerLines = 2;
protected string $separator = '|';
protected bool $removeLastRow = true;
}

View File

@ -1,7 +0,0 @@
<?php
namespace App\SSH\Services\Firewall;
use App\SSH\Services\AbstractService;
abstract class AbstractFirewall extends AbstractService implements Firewall {}

View File

@ -1,10 +0,0 @@
<?php
namespace App\SSH\Services\Firewall;
use App\SSH\Services\ServiceInterface;
interface Firewall extends ServiceInterface
{
public function applyRules(): void;
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\SSH\Services\Firewall;
use App\Enums\FirewallRuleStatus;
use App\Exceptions\SSHError;
class Ufw extends AbstractFirewall
{
/**
* @throws SSHError
*/
public function install(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.firewall.ufw.install-ufw'),
'install-ufw'
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
//
}
/**
* @throws SSHError
*/
public function applyRules(): void
{
$rules = $this->service->server
->firewallRules()
->where('status', '!=', FirewallRuleStatus::DELETING)
->get();
$this->service->server->ssh()->exec(
view('ssh.services.firewall.ufw.apply-rules', ['rules' => $rules]),
'apply-rules'
);
}
}

View File

@ -1,53 +0,0 @@
<?php
namespace App\SSH\Services\Monitoring\RemoteMonitor;
use App\Models\Metric;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Validation\Rule;
class RemoteMonitor extends AbstractService
{
public function creationRules(array $input): array
{
return [
'type' => [
function (string $attribute, mixed $value, Closure $fail): void {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'data_retention' => 10,
];
}
public function data(): array
{
return [
'data_retention' => $this->service->type_data['data_retention'] ?? 10,
];
}
public function install(): void
{
//
}
public function uninstall(): void
{
Metric::where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -1,99 +0,0 @@
<?php
namespace App\SSH\Services\Monitoring\VitoAgent;
use App\Exceptions\ServiceInstallationFailed;
use App\Exceptions\SSHError;
use App\Models\Metric;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rule;
use Ramsey\Uuid\Uuid;
class VitoAgent extends AbstractService
{
const TAGS_URL = 'https://api.github.com/repos/vitodeploy/agent/tags';
const DOWNLOAD_URL = 'https://github.com/vitodeploy/agent/releases/download/%s';
public function creationRules(array $input): array
{
return [
'type' => [
function (string $attribute, mixed $value, Closure $fail): void {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'url' => '',
'secret' => Uuid::uuid4()->toString(),
'data_retention' => 10,
];
}
public function data(): array
{
return [
'url' => $this->service->type_data['url'] ?? null,
'secret' => $this->service->type_data['secret'] ?? null,
'data_retention' => $this->service->type_data['data_retention'] ?? 10,
];
}
/**
* @throws SSHError
* @throws ServiceInstallationFailed
*/
public function install(): void
{
$tags = Http::get(self::TAGS_URL)->json();
if (empty($tags)) {
throw new ServiceInstallationFailed('Failed to fetch tags');
}
$this->service->version = $tags[0]['name'];
$this->service->save();
$downloadUrl = sprintf(self::DOWNLOAD_URL, $this->service->version);
$data = $this->data();
$data['url'] = route('api.servers.agent', [$this->service->server, $this->service->id]);
$this->service->type_data = $data;
$this->service->save();
$this->service->refresh();
$this->service->server->ssh()->exec(
view('ssh.services.monitoring.vito-agent.install', [
'downloadUrl' => $downloadUrl,
'configUrl' => $this->data()['url'],
'configSecret' => $this->data()['secret'],
]),
'install-vito-agent'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.monitoring.vito-agent.uninstall'),
'uninstall-vito-agent'
);
Metric::query()->where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -1,84 +0,0 @@
<?php
namespace App\SSH\Services\NodeJS;
use App\Exceptions\SSHError;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Validation\Rule;
class NodeJS extends AbstractService
{
public function creationRules(array $input): array
{
return [
'version' => [
'required',
Rule::in(config('core.nodejs_versions')),
Rule::notIn([\App\Enums\NodeJS::NONE]),
Rule::unique('services', 'version')
->where('type', 'nodejs')
->where('server_id', $this->service->server_id),
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail): void {
$hasSite = $this->service->server->sites()
->where('nodejs_version', $this->service->version)
->exists();
if ($hasSite) {
$fail('Some sites are using this NodeJS version.');
}
},
],
];
}
/**
* @throws SSHError
*/
public function install(): void
{
$server = $this->service->server;
$server->ssh()->exec(
view('ssh.services.nodejs.install-nodejs', [
'version' => $this->service->version,
]),
'install-nodejs-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.nodejs.uninstall-nodejs', [
'version' => $this->service->version,
'default' => $this->service->is_default,
]),
'uninstall-nodejs-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function setDefaultCli(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.nodejs.change-default-nodejs', [
'version' => $this->service->version,
]),
'change-default-nodejs'
);
}
}

View File

@ -1,162 +0,0 @@
<?php
namespace App\SSH\Services\PHP;
use App\Exceptions\SSHCommandError;
use App\Exceptions\SSHError;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PHP extends AbstractService
{
public function creationRules(array $input): array
{
return [
'version' => [
'required',
Rule::in(config('core.php_versions')),
Rule::notIn([\App\Enums\PHP::NONE]),
Rule::unique('services', 'version')
->where('type', 'php')
->where('server_id', $this->service->server_id),
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail): void {
$hasSite = $this->service->server->sites()
->where('php_version', $this->service->version)
->exists();
if ($hasSite) {
$fail('Some sites are using this PHP version.');
}
},
],
];
}
/**
* @throws SSHError
*/
public function install(): void
{
$server = $this->service->server;
$server->ssh()->exec(
view('ssh.services.php.install-php', [
'version' => $this->service->version,
'user' => $server->getSshUser(),
]),
'install-php-'.$this->service->version
);
$this->installComposer();
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.php.uninstall-php', [
'version' => $this->service->version,
]),
'uninstall-php-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function setDefaultCli(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.php.change-default-php', [
'version' => $this->service->version,
]),
'change-default-php'
);
}
/**
* @throws SSHError
*/
public function installExtension(string $name): void
{
$result = $this->service->server->ssh()->exec(
view('ssh.services.php.install-php-extension', [
'version' => $this->service->version,
'name' => $name,
]),
'install-php-extension-'.$name
);
$pos = strpos($result, '[PHP Modules]');
if ($pos === false) {
throw new SSHCommandError('Failed to install extension');
}
$result = Str::substr($result, $pos);
if (! Str::contains($result, $name)) {
throw new SSHCommandError('Failed to install extension');
}
}
/**
* @throws SSHError
*/
public function installComposer(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.php.install-composer'),
'install-composer'
);
}
/**
* @throws SSHError
*/
public function getPHPIni(string $type): string
{
return $this->service->server->os()->readFile(
sprintf('/etc/php/%s/%s/php.ini', $this->service->version, $type)
);
}
/**
* @throws SSHError
*/
public function createFpmPool(string $user, string $version): void
{
$this->service->server->ssh()->write(
"/etc/php/{$version}/fpm/pool.d/{$user}.conf",
view('ssh.services.php.fpm-pool', [
'user' => $user,
'version' => $version,
]),
'root'
);
$this->service->server->systemd()->restart($this->service->unit);
}
/**
* @throws SSHError
*/
public function removeFpmPool(string $user, string $version, ?int $siteId): void
{
$this->service->server->ssh()->exec(
view('ssh.services.php.remove-fpm-pool', [
'user' => $user,
'version' => $version,
]),
"remove-{$version}fpm-pool-{$user}",
$siteId
);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\SSH\Services\ProcessManager;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractProcessManager extends AbstractService implements ProcessManager
{
public function creationRules(array $input): array
{
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail): void {
$processManagerExists = $this->service->server->processManager();
if ($processManagerExists) {
$fail('You already have a process manager service on the server.');
}
},
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail): void {
$hasWorker = $this->service->server->workers()->exists();
if ($hasWorker) {
$fail('You have worker(s) on the server.');
}
},
],
];
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\SSH\Services\ProcessManager;
use App\SSH\Services\ServiceInterface;
interface ProcessManager extends ServiceInterface
{
public function create(
int $id,
string $command,
string $user,
bool $autoStart,
bool $autoRestart,
int $numprocs,
string $logFile,
?int $siteId = null
): void;
public function delete(int $id, ?int $siteId = null): void;
public function restart(int $id, ?int $siteId = null): void;
public function stop(int $id, ?int $siteId = null): void;
public function start(int $id, ?int $siteId = null): void;
public function getLogs(string $user, string $logPath): string;
}

View File

@ -1,137 +0,0 @@
<?php
namespace App\SSH\Services\ProcessManager;
use App\Exceptions\SSHError;
use Throwable;
class Supervisor extends AbstractProcessManager
{
/**
* @throws SSHError
*/
public function install(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.install-supervisor'),
'install-supervisor'
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.uninstall-supervisor'),
'uninstall-supervisor'
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function create(
int $id,
string $command,
string $user,
bool $autoStart,
bool $autoRestart,
int $numprocs,
string $logFile,
?int $siteId = null
): void {
$this->service->server->ssh()->write(
"/etc/supervisor/conf.d/$id.conf",
view('ssh.services.process-manager.supervisor.worker', [
'name' => (string) $id,
'command' => $command,
'user' => $user,
'autoStart' => var_export($autoStart, true),
'autoRestart' => var_export($autoRestart, true),
'numprocs' => (string) $numprocs,
'logFile' => $logFile,
]),
'root'
);
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.create-worker', [
'id' => $id,
'logFile' => $logFile,
'user' => $user,
]),
'create-worker',
$siteId
);
}
/**
* @throws Throwable
*/
public function delete(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.delete-worker', [
'id' => $id,
]),
'delete-worker',
$siteId
);
}
/**
* @throws Throwable
*/
public function restart(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.restart-worker', [
'id' => $id,
]),
'restart-worker',
$siteId
);
}
/**
* @throws Throwable
*/
public function stop(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.stop-worker', [
'id' => $id,
]),
'stop-worker',
$siteId
);
}
/**
* @throws Throwable
*/
public function start(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
view('ssh.services.process-manager.supervisor.start-worker', [
'id' => $id,
]),
'start-worker',
$siteId
);
}
/**
* @throws Throwable
*/
public function getLogs(string $user, string $logPath): string
{
return $this->service->server->ssh($user)->exec(
"tail -100 $logPath"
);
}
}

View File

@ -1,53 +0,0 @@
<?php
namespace App\SSH\Services\Redis;
use App\Exceptions\ServiceInstallationFailed;
use App\Exceptions\SSHError;
use App\SSH\Services\AbstractService;
use Closure;
class Redis extends AbstractService
{
public function creationRules(array $input): array
{
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail): void {
$redisExists = $this->service->server->memoryDatabase();
if ($redisExists) {
$fail('You already have a Redis service on the server.');
}
},
],
];
}
/**
* @throws ServiceInstallationFailed
* @throws SSHError
*/
public function install(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.redis.install'),
'install-redis'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.redis.uninstall'),
'uninstall-redis'
);
$this->service->server->os()->cleanup();
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\SSH\Services;
interface ServiceInterface
{
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function creationRules(array $input): array;
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function creationData(array $input): array;
/**
* @return array<string, mixed>
*/
public function deletionRules(): array;
/**
* @return array<string, mixed>
*/
public function data(): array;
public function install(): void;
public function uninstall(): void;
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\SSH\Services\Webserver;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractWebserver extends AbstractService implements Webserver
{
public function creationRules(array $input): array
{
return [
'name' => [
'required',
function (string $attribute, mixed $value, Closure $fail): void {
$webserverExists = $this->service->server->webserver();
if ($webserverExists) {
$fail('You already have a webserver service on the server.');
}
},
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail): void {
$hasSite = $this->service->server->sites()
->exists();
if ($hasSite) {
$fail('Cannot uninstall webserver while you have websites using it.');
}
},
],
];
}
}

View File

@ -1,200 +0,0 @@
<?php
namespace App\SSH\Services\Webserver;
use App\Exceptions\SSHError;
use App\Exceptions\SSLCreationException;
use App\Models\Site;
use App\Models\Ssl;
use Throwable;
class Caddy extends AbstractWebserver
{
public function name(): string
{
return \App\Enums\Webserver::CADDY;
}
/**
* @throws SSHError
*/
public function install(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.caddy.install-caddy'),
'install-caddy'
);
$this->service->server->ssh()->write(
'/etc/caddy/Caddyfile',
view('ssh.services.webserver.caddy.caddy'),
'root'
);
$this->service->server->ssh()->write(
'/etc/systemd/system/caddy.service',
view('ssh.services.webserver.caddy.caddy-systemd'),
'root'
);
$this->service->server->systemd()->reload();
$this->service->server->systemd()->restart('caddy');
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.caddy.uninstall-caddy'),
'uninstall-caddy'
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function createVHost(Site $site): void
{
// We need to get the isolated user first, if the site is isolated
// otherwise, use the default ssh user
$ssh = $this->service->server->ssh($site->user);
$ssh->exec(
view('ssh.services.webserver.caddy.create-path', [
'path' => $site->path,
]),
'create-path',
$site->id
);
$this->service->server->ssh()->write(
'/etc/caddy/sites-available/'.$site->domain,
$this->generateVhost($site),
'root'
);
$this->service->server->ssh()->exec(
view('ssh.services.webserver.caddy.create-vhost', [
'domain' => $site->domain,
]),
'create-vhost',
$site->id
);
}
/**
* @throws SSHError
*/
public function updateVHost(Site $site, ?string $vhost = null): void
{
$this->service->server->ssh()->write(
'/etc/caddy/sites-available/'.$site->domain,
$vhost ?? $this->generateVhost($site),
'root'
);
$this->service->server->systemd()->restart('caddy');
}
/**
* @throws SSHError
*/
public function getVHost(Site $site): string
{
return $this->service->server->ssh()->exec(
view('ssh.services.webserver.caddy.get-vhost', [
'domain' => $site->domain,
]),
);
}
/**
* @throws SSHError
*/
public function deleteSite(Site $site): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.caddy.delete-site', [
'domain' => $site->domain,
'path' => $site->path,
]),
'delete-vhost',
$site->id
);
$this->service->restart();
}
/**
* @throws SSHError
*/
public function changePHPVersion(Site $site, string $version): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.caddy.change-php-version', [
'domain' => $site->domain,
'oldVersion' => $site->php_version,
'newVersion' => $version,
]),
'change-php-version',
$site->id
);
}
/**
* @throws SSHError
*/
public function setupSSL(Ssl $ssl): void
{
if ($ssl->type == 'custom') {
$ssl->certificate_path = '/etc/ssl/'.$ssl->id.'/cert.pem';
$ssl->pk_path = '/etc/ssl/'.$ssl->id.'/privkey.pem';
$ssl->save();
$command = view('ssh.services.webserver.caddy.create-custom-ssl', [
'path' => dirname($ssl->certificate_path),
'certificate' => $ssl->certificate,
'pk' => $ssl->pk,
'certificatePath' => $ssl->certificate_path,
'pkPath' => $ssl->pk_path,
]);
$result = $this->service->server->ssh()->setLog($ssl->log)->exec(
$command,
'create-ssl',
$ssl->site_id
);
if (! $ssl->validateSetup($result)) {
throw new SSLCreationException;
}
}
}
/**
* @throws Throwable
*/
public function removeSSL(Ssl $ssl): void
{
if ($ssl->certificate_path) {
$this->service->server->ssh()->exec(
'sudo rm -rf '.dirname($ssl->certificate_path),
'remove-ssl',
$ssl->site_id
);
}
$this->updateVHost($ssl->site);
}
private function generateVhost(Site $site): string
{
$vhost = view('ssh.services.webserver.caddy.vhost', [
'site' => $site,
]);
return format_nginx_config($vhost);
}
}

View File

@ -1,204 +0,0 @@
<?php
namespace App\SSH\Services\Webserver;
use App\Exceptions\SSHError;
use App\Exceptions\SSLCreationException;
use App\Models\Site;
use App\Models\Ssl;
use Throwable;
class Nginx extends AbstractWebserver
{
public function name(): string
{
return \App\Enums\Webserver::NGINX;
}
/**
* @throws SSHError
*/
public function install(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.nginx.install-nginx'),
'install-nginx'
);
$this->service->server->ssh()->write(
'/etc/nginx/nginx.conf',
view('ssh.services.webserver.nginx.nginx', [
'user' => $this->service->server->getSshUser(),
]),
'root'
);
$this->service->server->systemd()->restart('nginx');
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.nginx.uninstall-nginx'),
'uninstall-nginx'
);
$this->service->server->os()->cleanup();
}
/**
* @throws SSHError
*/
public function createVHost(Site $site): void
{
// We need to get the isolated user first, if the site is isolated
// otherwise, use the default ssh user
$ssh = $this->service->server->ssh($site->user);
$ssh->exec(
view('ssh.services.webserver.nginx.create-path', [
'path' => $site->path,
]),
'create-path',
$site->id
);
$this->service->server->ssh()->write(
'/etc/nginx/sites-available/'.$site->domain,
$this->generateVhost($site),
'root'
);
$this->service->server->ssh()->exec(
view('ssh.services.webserver.nginx.create-vhost', [
'domain' => $site->domain,
'vhost' => $this->generateVhost($site),
]),
'create-vhost',
$site->id
);
}
/**
* @throws SSHError
*/
public function updateVHost(Site $site, ?string $vhost = null): void
{
$this->service->server->ssh()->write(
'/etc/nginx/sites-available/'.$site->domain,
$vhost ?? $this->generateVhost($site),
'root'
);
$this->service->server->systemd()->restart('nginx');
}
/**
* @throws SSHError
*/
public function getVHost(Site $site): string
{
return $this->service->server->ssh()->exec(
view('ssh.services.webserver.nginx.get-vhost', [
'domain' => $site->domain,
]),
);
}
/**
* @throws SSHError
*/
public function deleteSite(Site $site): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.nginx.delete-site', [
'domain' => $site->domain,
'path' => $site->path,
]),
'delete-vhost',
$site->id
);
$this->service->restart();
}
/**
* @throws SSHError
*/
public function changePHPVersion(Site $site, string $version): void
{
$this->service->server->ssh()->exec(
view('ssh.services.webserver.nginx.change-php-version', [
'domain' => $site->domain,
'oldVersion' => $site->php_version,
'newVersion' => $version,
]),
'change-php-version',
$site->id
);
}
/**
* @throws SSHError
*/
public function setupSSL(Ssl $ssl): void
{
$domains = '';
foreach ($ssl->getDomains() as $domain) {
$domains .= ' -d '.$domain;
}
$command = view('ssh.services.webserver.nginx.create-letsencrypt-ssl', [
'email' => $ssl->email,
'name' => $ssl->id,
'domains' => $domains,
]);
if ($ssl->type == 'custom') {
$ssl->certificate_path = '/etc/ssl/'.$ssl->id.'/cert.pem';
$ssl->pk_path = '/etc/ssl/'.$ssl->id.'/privkey.pem';
$ssl->save();
$command = view('ssh.services.webserver.nginx.create-custom-ssl', [
'path' => dirname($ssl->certificate_path),
'certificate' => $ssl->certificate,
'pk' => $ssl->pk,
'certificatePath' => $ssl->certificate_path,
'pkPath' => $ssl->pk_path,
]);
}
$result = $this->service->server->ssh()->setLog($ssl->log)->exec(
$command,
'create-ssl',
$ssl->site_id
);
if (! $ssl->validateSetup($result)) {
throw new SSLCreationException;
}
}
/**
* @throws Throwable
*/
public function removeSSL(Ssl $ssl): void
{
if ($ssl->certificate_path) {
$this->service->server->ssh()->exec(
'sudo rm -rf '.dirname($ssl->certificate_path),
'remove-ssl',
$ssl->site_id
);
}
$this->updateVHost($ssl->site);
}
private function generateVhost(Site $site): string
{
$vhost = view('ssh.services.webserver.nginx.vhost', [
'site' => $site,
]);
return format_nginx_config($vhost);
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\SSH\Services\Webserver;
use App\Models\Site;
use App\Models\Ssl;
use App\SSH\Services\ServiceInterface;
interface Webserver extends ServiceInterface
{
public function name(): string;
public function createVHost(Site $site): void;
public function updateVHost(Site $site, ?string $vhost = null): void;
public function getVHost(Site $site): string;
public function deleteSite(Site $site): void;
public function changePHPVersion(Site $site, string $version): void;
public function setupSSL(Ssl $ssl): void;
public function removeSSL(Ssl $ssl): void;
}

View File

@ -4,13 +4,10 @@
use App\Exceptions\SSHCommandError;
use App\Exceptions\SSHError;
use App\SSH\HasS3Storage;
use Illuminate\Support\Facades\Log;
class S3 extends AbstractStorage
{
use HasS3Storage;
/**
* @throws SSHError
*/
@ -89,4 +86,25 @@ public function delete(string $src): void
'delete-from-s3'
);
}
/**
* @throws SSHError
*/
private function prepareS3Path(string $path, string $prefix = ''): string
{
$path = trim($path);
$path = ltrim($path, '/');
$path = preg_replace('/[^a-zA-Z0-9\-_\.\/]/', '_', $path);
$path = preg_replace('/\/+/', '/', (string) $path);
if ($prefix !== '' && $prefix !== '0') {
return trim($prefix, '/').'/'.$path;
}
if ($path === null) {
throw new SSHError('Invalid S3 path');
}
return $path;
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\SSH\Wordpress;
use App\Exceptions\SSHError;
use App\Models\Site;
class Wordpress
{
/**
* @throws SSHError
*/
public function install(Site $site): void
{
$site->server->ssh($site->user)->exec(
view('ssh.wordpress.install', [
'path' => $site->path,
'domain' => $site->domain,
'isIsolated' => $site->isIsolated() ? 'true' : 'false',
'isolatedUsername' => $site->user,
'dbName' => $site->type_data['database'],
'dbUser' => $site->type_data['database_user'],
'dbPass' => $site->type_data['database_password'],
'dbHost' => 'localhost',
'dbPrefix' => 'wp_',
'username' => $site->type_data['username'],
'password' => $site->type_data['password'],
'email' => $site->type_data['email'],
'title' => $site->type_data['title'],
]),
'install-wordpress',
$site->id
);
}
}