Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions app/Actions/Site/UpdateBasicAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@

use App\Helpers\Apr1Hasher;
use App\Models\Site;
use App\Services\Webserver\Nginx;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class UpdateBasicAuth
{
private const NGINX_AUTH_DIR = '/etc/nginx/auth';

/**
* @param array<string, mixed> $input
*/
Expand Down Expand Up @@ -118,15 +115,15 @@ private function validate(Site $site, array $input): void
*/
private function writeAuthFile(Site $site, array $users): void
{
if ($site->webserver()::id() !== Nginx::id()) {
$path = $site->htpasswdPath();

if ($path === null) {
return;
}

$path = $site->htpasswdPath();

if (empty($users)) {
$site->server->ssh()->exec(
view('ssh.services.webserver.nginx.remove-basic-auth-file', ['path' => $path]),
view('ssh.services.webserver.shared.remove-basic-auth-file', ['path' => $path]),
'remove-basic-auth-file',
$site->id,
);
Expand All @@ -138,13 +135,13 @@ private function writeAuthFile(Site $site, array $users): void
$usernames = implode(', ', array_map(fn (array $u) => $u['username'], $users));

$site->server->ssh()->exec(
view('ssh.services.webserver.nginx.write-basic-auth-file', [
'dir' => self::NGINX_AUTH_DIR,
view('ssh.services.webserver.shared.write-basic-auth-file', [
'dir' => dirname($path),
'path' => $path,
'lines' => $lines,
'userCount' => count($users),
'usernames' => $usernames,
'nginxUser' => $site->server->getSshUser(),
'webserverUser' => $site->server->getSshUser(),
]),
'write-basic-auth-file',
$site->id,
Expand Down
8 changes: 1 addition & 7 deletions app/Actions/Site/UpdateVhostTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace App\Actions\Site;

use App\Actions\Webserver\GenerateCaddyConfig;
use App\Actions\Webserver\GenerateNginxConfig;
use App\Models\Site;

class UpdateVhostTemplate
Expand All @@ -27,10 +25,6 @@ public function update(Site $site, array $input): void

private function matchesDefault(Site $site, string $template): bool
{
$generator = $site->webserver()::id() === 'caddy'
? app(GenerateCaddyConfig::class)
: app(GenerateNginxConfig::class);

return rtrim($template) === rtrim($generator->defaultTemplate());
return rtrim($template) === rtrim($site->webserver()->configGenerator()->defaultTemplate());
}
}
13 changes: 11 additions & 2 deletions app/Actions/Webserver/AbstractGenerateConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ public function generate(Site $site, ?string $template = null): string
'escape' => fn ($value) => $value,
]);

return format_webserver_config($engine->render($template, $data));
return $this->formatConfig($engine->render($template, $data));
}

/**
* Format the rendered config. Brace-indented webservers use the default;
* tag-based webservers (e.g. Apache) override this.
*/
protected function formatConfig(string $config): string
{
return format_webserver_config($config);
}

/**
Expand Down Expand Up @@ -216,7 +225,7 @@ protected function buildCommonData(Site $site, string $primaryDomain): array
'type_data' => $site->type_data ?? [],
'basic_auth_enabled' => $basicAuthEnabled,
'basic_auth_realm' => $site->domain,
'basic_auth_file' => $site->htpasswdPath(),
'basic_auth_file' => $site->htpasswdPath() ?? '',
'basic_auth_users' => $basicAuthEnabled ? array_values($basicAuth['users']) : [],
'verification_key' => $site->verification_key,
];
Expand Down
183 changes: 183 additions & 0 deletions app/Actions/Webserver/GenerateApacheConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

namespace App\Actions\Webserver;

use App\Enums\LoadBalancerMethod;
use App\Models\HostedDomain;
use App\Models\Site;

class GenerateApacheConfig extends AbstractGenerateConfig
{
/** @var array<int, string> Collected during server block building */
private array $forceSSLDomains = [];

public function defaultTemplate(): string
{
return file_get_contents(resource_path('views/ssh/services/webserver/apache/vhost.mustache'));
}

protected function buildServerBlockKeys(bool $hasSsl, string $sslCertPath, string $sslKeyPath, Site $site): array
{
return [
'listen_80' => $hasSsl ? ! $site->force_ssl : true,
'listen_443' => $hasSsl,
'ssl_certificate_path' => $sslCertPath,
'ssl_certificate_key_path' => $sslKeyPath,
];
}

protected function buildPhpSocket(Site $site): string
{
if ($site->isIsolated()) {
return "unix:/run/php/php{$site->php_version}-fpm-{$site->user}.sock|fcgi://localhost";
}

return "unix:/var/run/php/php{$site->php_version}-fpm.sock|fcgi://localhost";
}

protected function buildLoadBalancerData(Site $site): array
{
$balancerName = preg_replace('/[^A-Za-z0-9]/', '', $site->domain).'_balancer';
$isLoadBalancer = $site->type === 'load-balancer';

$data = [
'balancer_name' => $balancerName,
'lb_method' => 'byrequests',
'lb_servers' => [],
];

if ($isLoadBalancer) {
$method = $site->type_data['method'] ?? LoadBalancerMethod::ROUND_ROBIN->value;
$data['lb_method'] = match ($method) {
LoadBalancerMethod::LEAST_CONNECTIONS->value => 'bybusyness',
default => 'byrequests',
};

$data['lb_servers'] = $site->loadBalancerServers->map(fn ($s) => [
'address' => $s->ip.':'.$s->port,
])->all();
}

return $data;
}

protected function buildRedirectEntry(object $redirect, bool $isProxy): array
{
return [
'from' => $redirect->from,
'to' => $redirect->to,
'mode' => $redirect->mode,
'is_proxy' => $isProxy,
];
}

protected function transformDomains(array $domains, bool $httpOnly): array
{
return $domains;
}

protected function enrichServerBlock(array $block, array $data): array
{
$names = array_map(fn (array $domain) => $domain['name'], $block['domains']);

$block['server_name'] = $names[0] ?? $data['primary_domain'];
$block['server_aliases'] = array_map(fn (string $name) => ['name' => $name], array_slice($names, 1));
$block['balancer_name'] = $data['balancer_name'];
$block['lb_method'] = $data['lb_method'];
$block['lb_servers'] = $data['lb_servers'];

return $block;
}

protected function buildRedirectBlock(HostedDomain $hd, string $primaryDomain, Site $site): array
{
$hasSsl = $hd->ssl_id && $hd->ssl;
$redirectScheme = $site->ssl_enabled ? 'https' : 'http';

return [
'listen_80' => true,
'listen_443' => (bool) $hasSsl,
'ssl_certificate_path' => $hasSsl ? $hd->ssl->certificate_path : '',
'ssl_certificate_key_path' => $hasSsl ? $hd->ssl->pk_path : '',
'server_name' => $hd->domain,
'redirect_target' => $primaryDomain,
'redirect_scheme' => $redirectScheme,
];
}

protected function buildData(Site $site): array
{
$this->forceSSLDomains = [];

return parent::buildData($site);
}

protected function finalizeData(array $data, Site $site): array
{
if ($site->force_ssl && $site->ssl_enabled) {
foreach ($data['server_blocks'] as $block) {
if ($block['listen_443'] ?? false) {
foreach ($block['domains'] as $domain) {
$this->forceSSLDomains[] = $domain['name'];
}
}
}
}

$this->forceSSLDomains = array_values(array_unique($this->forceSSLDomains));

$data['vhosts'] = $this->expandVhosts($data['server_blocks']);
$data['redirect_vhosts'] = $this->expandVhosts($data['redirect_blocks']);

$data['has_force_ssl_redirect'] = ! empty($this->forceSSLDomains);
$data['force_ssl_server_name'] = $this->forceSSLDomains[0] ?? '';
$data['force_ssl_aliases'] = array_map(fn (string $name) => ['name' => $name], array_slice($this->forceSSLDomains, 1));
$data['force_ssl_root'] = $site->getWebDirectoryPath();

return $data;
}

protected function formatConfig(string $config): string
{
$lines = explode("\n", trim($config));
$formatted = [];
$lastWasEmpty = false;

foreach ($lines as $line) {
$line = rtrim($line);
$isEmpty = trim($line) === '';

if ($isEmpty && $lastWasEmpty) {
continue;
}

$formatted[] = $line;
$lastWasEmpty = $isEmpty;
}

return implode("\n", $formatted)."\n";
}

/**
* Expand brace-style server blocks into one entry per listen port.
*
* @param array<int, array<string, mixed>> $blocks
* @return array<int, array<string, mixed>>
*/
private function expandVhosts(array $blocks): array
{
$vhosts = [];

foreach ($blocks as $block) {
if ($block['listen_80'] ?? false) {
$vhosts[] = ['listen_port' => 80, 'has_ssl' => false] + $block;
}

if ($block['listen_443'] ?? false) {
$vhosts[] = ['listen_port' => 443, 'has_ssl' => true] + $block;
}
}

return $vhosts;
}
}
13 changes: 1 addition & 12 deletions app/Http/Controllers/SiteSettingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
use App\Actions\Site\UpdateVhostTemplate;
use App\Actions\Site\UpdateWebDirectory;
use App\Actions\Site\WorkerStartCommandUpdateResult;
use App\Actions\Webserver\GenerateCaddyConfig;
use App\Actions\Webserver\GenerateNginxConfig;
use App\Exceptions\SSHError;
use App\Http\Resources\SourceControlResource;
use App\Models\Server;
Expand Down Expand Up @@ -166,10 +164,8 @@ public function vhostTemplate(Server $server, Site $site): JsonResponse
{
$this->authorize('update', [$site, $server]);

$generator = $this->getVhostGenerator($site);

return response()->json([
'template' => $site->vhost_template ?? $generator->defaultTemplate(),
'template' => $site->vhost_template ?? $site->webserver()->configGenerator()->defaultTemplate(),
]);
}

Expand Down Expand Up @@ -218,13 +214,6 @@ public function updateVhostGeneration(Request $request, Server $server, Site $si
return back()->with('success', 'VHost generation setting updated successfully.');
}

private function getVhostGenerator(Site $site): GenerateNginxConfig|GenerateCaddyConfig
{
return $site->webserver()::id() === 'caddy'
? app(GenerateCaddyConfig::class)
: app(GenerateNginxConfig::class);
}

#[Post('/force-ssl/enable', name: 'site-settings.enable-force-ssl')]
public function enableForceSsl(Server $server, Site $site): RedirectResponse
{
Expand Down
10 changes: 8 additions & 2 deletions app/Models/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -758,9 +758,15 @@ public function basePath(): string
return preg_replace('#/current$#', '', $this->path);
}

public function htpasswdPath(): string
public function htpasswdPath(): ?string
{
return '/etc/nginx/auth/site-'.$this->id.'.htpasswd';
if (! $this->server->webserver()) {
return null;
}

$dir = $this->webserver()->basicAuthDir();

return $dir ? $dir.'/site-'.$this->id.'.htpasswd' : null;
}

public function getDeployKeyName(): string
Expand Down
15 changes: 15 additions & 0 deletions app/Providers/ServiceTypeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Services\ProcessManager\Supervisor;
use App\Services\Redis\Redis;
use App\Services\Valkey\Valkey;
use App\Services\Webserver\Apache;
use App\Services\Webserver\Caddy;
use App\Services\Webserver\Nginx;
use Illuminate\Support\ServiceProvider;
Expand Down Expand Up @@ -56,6 +57,20 @@ private function webservers(): void
->handler(Caddy::class)
->data(['creates_site_ssls' => false])
->register();

RegisterServiceType::make(Apache::id())
->type(Apache::type())
->label('Apache (beta)')
->handler(Apache::class)
->data(['creates_site_ssls' => true])
->configPaths([
[
'name' => 'apache2.conf',
'path' => '/etc/apache2/apache2.conf',
'sudo' => true,
],
])
->register();
}

private function databases(): void
Expand Down
5 changes: 5 additions & 0 deletions app/Services/Webserver/AbstractWebserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

abstract class AbstractWebserver extends AbstractService implements Webserver
{
public static function type(): string
{
return 'webserver';
}

public function creationRules(array $input): array
{
return [
Expand Down
Loading
Loading