Merge commit 'aab4a0c7b5edab1ca261a528e3407ff2f1b59297'

This commit is contained in:
Buster "Silver Eagle" Neece 2022-06-09 02:27:19 -05:00
parent 18fe4060e7
commit 1e3ccd93d5
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
44 changed files with 673 additions and 555 deletions

View File

@ -5,7 +5,9 @@ release channel, you can take advantage of these new features and fixes.
## New Features/Changes
There have been no new features/changes since the last stable release.
- **LetsEncrypt via the Web**: We now support configuring LetsEncrypt via the web interface. If you had previously set
up LetsEncrypt via the command line, your settings will be imported automatically. This update also adds LetsEncrypt
support for Ansible installations.
## Code Quality/Technical Changes

View File

@ -0,0 +1 @@
{"users":[],"groups":[],"folders":[],"admins":[],"api_keys":[],"shares":[],"version":12}

View File

@ -0,0 +1 @@
{"users":[{"id":1,"status":1,"username":"test","expiration_date":0,"password":"$2a$10$1ULW1yY/Vwxzbl77gCoyGu.rzPROEv6Z07n9/D.FcLneJJ35Z8o52","home_dir":"/var/azuracast/stations/azuratest_radio/media","uid":0,"gid":0,"max_sessions":0,"quota_size":1000000000,"quota_files":0,"permissions":{"/":["*"]},"upload_data_transfer":0,"download_data_transfer":0,"total_data_transfer":0,"last_login":1654581296256,"created_at":1654581296198,"updated_at":1654581296198,"filters":{"hooks":{"external_auth_disabled":false,"pre_login_disabled":false,"check_password_disabled":false},"totp_config":{"secret":{}}},"filesystem":{"provider":0,"s3config":{"access_secret":{}},"gcsconfig":{"credentials":{}},"azblobconfig":{"account_key":{},"sas_url":{}},"cryptconfig":{"passphrase":{}},"sftpconfig":{"password":{},"private_key":{},"key_passphrase":{}}}}],"groups":[],"folders":[],"admins":[],"api_keys":[],"shares":[],"version":12}

View File

@ -70,6 +70,7 @@
"psr/simple-cache": ">1",
"ramsey/uuid": "^4.0",
"rlanvin/php-ip": "dev-master",
"skoerfgen/acmecert": "^3.2",
"slim/http": "^1.1",
"slim/slim": "^4.2",
"spatie/flysystem-dropbox": "^2",

46
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d0f53f37fc280407f5070076e53e3bc2",
"content-hash": "13899e60907b126bd3c5b7ea5cbf1278",
"packages": [
{
"name": "aws/aws-crt-php",
@ -6242,6 +6242,50 @@
},
"time": "2022-03-02T08:51:37+00:00"
},
{
"name": "skoerfgen/acmecert",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/skoerfgen/ACMECert.git",
"reference": "706564824eed25896b2e02d80095bd82c051fc2d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/skoerfgen/ACMECert/zipball/706564824eed25896b2e02d80095bd82c051fc2d",
"reference": "706564824eed25896b2e02d80095bd82c051fc2d",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"php": ">=5.6.0"
},
"suggest": {
"ext-curl": "Optional for better http performance"
},
"type": "library",
"autoload": {
"psr-4": {
"skoerfgen\\ACMECert\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Stefan Körfgen",
"homepage": "https://github.com/skoerfgen"
}
],
"description": "PHP client library for Let's Encrypt and other ACME v2 - RFC 8555 compatible Certificate Authorities",
"support": {
"issues": "https://github.com/skoerfgen/ACMECert/issues",
"source": "https://github.com/skoerfgen/ACMECert"
},
"time": "2022-04-22T23:10:20+00:00"
},
{
"name": "slim/http",
"version": "1.2.0",

View File

@ -4,6 +4,7 @@ use App\Console\Command;
return function (App\Event\BuildConsoleCommands $event) {
$event->addAliases([
'azuracast:acme:get-certificate' => Command\Acme\GetCertificateCommand::class,
'azuracast:backup' => Command\Backup\BackupCommand::class,
'azuracast:restore' => Command\Backup\RestoreCommand::class,
'azuracast:debug:optimize-tables' => Command\Debug\OptimizeTablesCommand::class,
@ -37,5 +38,6 @@ return function (App\Event\BuildConsoleCommands $event) {
'queue:process' => Command\MessageQueue\ProcessCommand::class,
'queue:clear' => Command\MessageQueue\ClearCommand::class,
'cache:clear' => Command\ClearCacheCommand::class,
'acme:cert' => Command\Acme\GetCertificateCommand::class,
]);
};

View File

@ -138,6 +138,7 @@ return function (CallableEventDispatcherInterface $dispatcher) {
App\Sync\Task\MoveBroadcastsTask::class,
App\Sync\Task\QueueInterruptingTracks::class,
App\Sync\Task\ReactivateStreamerTask::class,
App\Sync\Task\RenewAcmeCertTask::class,
App\Sync\Task\RotateLogsTask::class,
App\Sync\Task\RunAnalyticsTask::class,
App\Sync\Task\RunAutomatedAssignmentTask::class,

View File

@ -13,6 +13,8 @@ return [
Message\BackupMessage::class => Task\RunBackupTask::class,
Message\GenerateAcmeCertificate::class => App\Service\Acme::class,
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
Message\TestWebhookMessage::class => App\Webhook\Dispatcher::class,

View File

@ -89,6 +89,16 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Admin\SendTestMessageAction::class
)->setName('api:admin:send-test-message');
$group->put(
'/acme',
Controller\Api\Admin\Acme\GenerateCertificateAction::class
)->setName('api:admin:acme');
$group->get(
'/acme-log/{path}',
Controller\Api\Admin\Acme\CertificateLogAction::class
)->setName('api:admin:acme-log');
$group->get(
'/custom_assets/{type}',
Controller\Api\Admin\CustomAssets\GetCustomAssetAction::class

View File

@ -10,7 +10,8 @@ services:
- "127.0.0.1:3306:3306"
- "127.0.0.1:6379:6379"
volumes:
- $PWD/util/local_ssl:/etc/nginx/certs
- $PWD/util/local_ssl/default.crt:/var/azuracast/acme/ssl.crt
- $PWD/util/local_ssl/default.key:/var/azuracast/acme/ssl.key
- $PWD/vendor:/var/azuracast/www/vendor
- $PWD:/var/azuracast/www
extra_hosts:

View File

@ -183,8 +183,6 @@ services:
PUID: ${AZURACAST_PUID:-1000}
PGID: ${AZURACAST_PGID:-1000}
volumes:
- letsencrypt:/etc/nginx/certs
- letsencrypt_acme:/etc/acme.sh
- www_uploads:/var/azuracast/uploads
- station_data:/var/azuracast/stations
- shoutcast2_install:/var/azuracast/servers/shoutcast2
@ -192,6 +190,7 @@ services:
- geolite_install:/var/azuracast/geoip
- sftpgo_data:/var/azuracast/sftpgo/persist
- backups:/var/azuracast/backups
- acme:/var/azuracast/acme
- db_data:/var/lib/mysql
restart: unless-stopped
ulimits: &default-ulimits
@ -205,8 +204,7 @@ services:
volumes:
db_data: { }
letsencrypt: { }
letsencrypt_acme: { }
acme: { }
shoutcast2_install: { }
stereo_tool_install: { }
geolite_install: { }

View File

@ -200,14 +200,6 @@ setup-ports() {
envfile-set "AZURACAST_SFTP_PORT" "2022" "Port to use for SFTP connections"
}
#
# Configure the settings used by LetsEncrypt.
#
setup-letsencrypt() {
envfile-set "LETSENCRYPT_HOST" "" "Domain name (example.com) or names (example.com,foo.bar) to use with LetsEncrypt"
envfile-set "LETSENCRYPT_EMAIL" "" "Optional e-mail address for expiration updates"
}
#
# Configure release mode settings.
#
@ -792,13 +784,14 @@ uninstall() {
}
#
# Create and link a LetsEncrypt SSL certificate.
# Usage: ./docker.sh letsencrypt-create
# LetsEncrypt: Now managed via the Web UI.
#
setup-letsencrypt() {
echo "LetsEncrypt is now managed from within the web interface."
}
letsencrypt-create() {
setup-letsencrypt
docker-compose down
docker-compose up -d
exit
}

View File

@ -23,7 +23,8 @@
:tab-class="getTabClass($v.securityPrivacyTab)"></settings-security-privacy-tab>
<settings-services-tab :form="$v.form" :tab-class="getTabClass($v.servicesTab)"
:release-channel="releaseChannel"
:test-message-url="testMessageUrl"></settings-services-tab>
:test-message-url="testMessageUrl"
:acme-url="acmeUrl"></settings-services-tab>
</b-tabs>
</b-overlay>
@ -52,6 +53,7 @@ export default {
props: {
apiUrl: String,
testMessageUrl: String,
acmeUrl: String,
releaseChannel: {
type: String,
default: 'rolling',
@ -84,6 +86,8 @@ export default {
api_access_control: {},
check_for_updates: {},
acme_email: {},
acme_domains: {},
mail_enabled: {},
mail_sender_name: {},
mail_sender_email: {},
@ -106,7 +110,9 @@ export default {
'form.analytics', 'form.always_use_ssl', 'form.api_access_control'
],
servicesTab: [
'form.check_for_updates', 'form.mail_enabled', 'form.mail_sender_name', 'form.mail_sender_email',
'form.check_for_updates',
'form.acme_email', 'form.acme_domains',
'form.mail_enabled', 'form.mail_sender_name', 'form.mail_sender_email',
'form.mail_smtp_host', 'form.mail_smtp_port', 'form.mail_smtp_secure', 'form.mail_smtp_username',
'form.mail_smtp_password', 'form.avatar_service', 'form.avatar_default_url',
'form.use_external_album_art_in_apis', 'form.use_external_album_art_when_processing_media',
@ -148,6 +154,8 @@ export default {
api_access_control: data.api_access_control,
check_for_updates: data.check_for_updates,
acme_email: data.acme_email,
acme_domains: data.acme_domains,
mail_enabled: data.mail_enabled,
mail_sender_name: data.mail_sender_name,
mail_sender_email: data.mail_sender_email,

View File

@ -35,6 +35,49 @@
</b-form-row>
</b-form-fieldset>
<b-form-fieldset>
<template #label>
<translate key="lang_section_letsencrypt">LetsEncrypt</translate>
</template>
<template #description>
<translate key="lang_section_letsencrypt_desc">LetsEncrypt provides simple, free SSL certificates allowing you to secure traffic through your control panel and radio streams.</translate>
</template>
<b-form-row>
<b-wrapped-form-group class="col-md-6" id="edit_form_acme_email"
:field="form.acme_email" input-type="email">
<template #label="{lang}">
<translate :key="lang">E-mail Address</translate>
</template>
<template #description="{lang}">
<translate
:key="lang">Enter your e-mail address to receive updates about your certificate.</translate>
</template>
</b-wrapped-form-group>
<b-wrapped-form-group class="col-md-6" id="edit_form_acme_domains"
:field="form.acme_domains">
<template #label="{lang}">
<translate :key="lang">Domain Name(s)</translate>
</template>
<template #description="{lang}">
<translate
:key="lang">All listed domain names should point to this AzuraCast installation. Separate multiple domain names with commas.</translate>
</template>
</b-wrapped-form-group>
<div class="form-group col">
<b-button size="sm" variant="primary" :disabled="form.$anyDirty" @click="generateAcmeCert">
<icon icon="badge"></icon>
<translate key="lang_btn_acme_cert">Generate/Renew Certificate</translate>
<span v-if="form.$anyDirty">
(<translate key="lang_btn_acme_cert_save_changes">Save Changes first</translate>)
</span>
</b-button>
</div>
</b-form-row>
</b-form-fieldset>
<b-form-fieldset>
<template #label>
<translate key="lang_section_email_delivery">E-mail Delivery Service</translate>
@ -177,6 +220,8 @@
</b-form-row>
</b-form-fieldset>
<streaming-log-modal ref="acmeModal"></streaming-log-modal>
<admin-settings-test-message-modal :test-message-url="testMessageUrl"></admin-settings-test-message-modal>
</b-tab>
</template>
@ -188,18 +233,25 @@ import BFormFieldset from "~/components/Form/BFormFieldset";
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
import AdminSettingsTestMessageModal from "~/components/Admin/Settings/TestMessageModal";
import Icon from "~/components/Common/Icon";
import StreamingLogModal from "~/components/Common/StreamingLogModal";
export default {
name: 'SettingsServicesTab',
components: {
StreamingLogModal,
Icon,
AdminSettingsTestMessageModal, BWrappedFormCheckbox, BFormFieldset, BWrappedFormGroup, BFormMarkup
AdminSettingsTestMessageModal,
BWrappedFormCheckbox,
BFormFieldset,
BWrappedFormGroup,
BFormMarkup
},
props: {
form: Object,
tabClass: {},
releaseChannel: String,
testMessageUrl: String,
acmeUrl: String,
},
computed: {
langTabTitle() {
@ -226,6 +278,15 @@ export default {
}
]
},
},
methods: {
generateAcmeCert() {
this.$wrapWithLoading(
this.axios.put(this.acmeUrl)
).then((resp) => {
this.$refs.acmeModal.show(resp.data.links.log);
});
}
}
}
</script>

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Console\Command\Acme;
use App\Console\Command\CommandAbstract;
use App\Service\Acme;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:acme:get-certificate',
description: 'Get a new or updated ACME (LetsEncrypt) certificate.',
aliases: ['acme:cert']
)]
final class GetCertificateCommand extends CommandAbstract
{
public function __construct(
private readonly Acme $acme
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
try {
$this->acme->getCertificate();
} catch (\Exception $e) {
$io->error($e->getMessage());
return 1;
}
return 0;
}
}

View File

@ -6,6 +6,7 @@ namespace App\Console\Command;
use App\Entity;
use App\Environment;
use App\Service\Acme;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -20,6 +21,7 @@ class InitializeCommand extends CommandAbstract
public function __construct(
protected Environment $environment,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
protected Acme $acme,
) {
parent::__construct();
}
@ -68,6 +70,13 @@ class InitializeCommand extends CommandAbstract
// Ensure default storage locations exist.
$this->storageLocationRepo->createDefaultStorageLocations();
// Pull Acme certificates if necessary.
try {
$this->acme->getCertificate();
} catch (\Exception) {
// Noop
}
$io->newLine();
$io->success(
[

View File

@ -33,6 +33,7 @@ final class SettingsAction
'group' => Settings::GROUP_GENERAL,
]),
'testMessageUrl' => (string)$router->named('api:admin:send-test-message'),
'acmeUrl' => (string)$router->named('api:admin:acme'),
'releaseChannel' => $this->version->getReleaseChannelEnum()->value,
],
);

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Admin\Acme;
use App\Controller\Api\Traits\HasLogViewer;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Utilities\File;
use Psr\Http\Message\ResponseInterface;
final class CertificateLogAction
{
use HasLogViewer;
public function __invoke(
ServerRequest $request,
Response $response,
string $path
): ResponseInterface {
$tempPath = File::validateTempPath($path);
return $this->streamLogToResponse(
$request,
$response,
$tempPath
);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Admin\Acme;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Message\GenerateAcmeCertificate;
use App\Utilities\File;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Messenger\MessageBus;
final class GenerateCertificateAction
{
public function __construct(
private readonly MessageBus $messageBus
) {
}
public function __invoke(
ServerRequest $request,
Response $response
): ResponseInterface {
$tempFile = File::generateTempPath('acme_test.log');
$message = new GenerateAcmeCertificate();
$message->outputPath = $tempFile;
$this->messageBus->dispatch($message);
$router = $request->getRouter();
return $response->withJson(
[
'success' => true,
'links' => [
'log' => (string)$router->fromHere('api:admin:acme-log', [
'path' => basename($tempFile),
]),
],
]
);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Entity\Migration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220608113502 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add ACME settings to Settings table.';
}
public function up(Schema $schema): void
{
$this->addSql(
'ALTER TABLE settings ADD acme_email VARCHAR(255) DEFAULT NULL, ADD acme_domains VARCHAR(255) DEFAULT NULL'
);
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE settings DROP acme_email, DROP acme_domains');
}
}

View File

@ -75,6 +75,18 @@ final class StationRepository extends Repository
return $select;
}
/**
* @return iterable<Entity\Station>
*/
public function iterateEnabledStations(): iterable
{
return $this->em->createQuery(
<<<DQL
SELECT s FROM App\Entity\Station s WHERE s.is_enabled = 1
DQL
)->toIterable();
}
/**
* @param string $short_code
*/

View File

@ -1006,6 +1006,40 @@ class Settings implements Stringable
$this->avatar_default_url = $avatarDefaultUrl;
}
#[
OA\Property(description: "ACME (LetsEncrypt) e-mail address.", example: ""),
ORM\Column(length: 255, nullable: true),
Groups(self::GROUP_GENERAL)
]
protected ?string $acme_email = null;
public function getAcmeEmail(): ?string
{
return $this->acme_email;
}
public function setAcmeEmail(?string $acme_email): void
{
$this->acme_email = $acme_email;
}
#[
OA\Property(description: "ACME (LetsEncrypt) domain name(s).", example: ""),
ORM\Column(length: 255, nullable: true),
Groups(self::GROUP_GENERAL)
]
protected ?string $acme_domains = null;
public function getAcmeDomains(): ?string
{
return $this->acme_domains;
}
public function setAcmeDomains(?string $acme_domains): void
{
$this->acme_domains = $acme_domains;
}
public function __toString(): string
{
return 'Settings';

View File

@ -241,23 +241,6 @@ class InstallCommand extends Command
$env['AZURACAST_STATION_PORTS'] = implode(',', $stationPorts);
}
$customizeLetsEncrypt = $io->confirm(
__('Set up LetsEncrypt?'),
false
);
if ($customizeLetsEncrypt) {
$env['LETSENCRYPT_HOST'] = $io->ask(
$envConfig['LETSENCRYPT_HOST']['description'],
$env['LETSENCRYPT_HOST'] ?? ''
);
$env['LETSENCRYPT_EMAIL'] = $io->ask(
$envConfig['LETSENCRYPT_EMAIL']['description'],
$env['LETSENCRYPT_EMAIL'] ?? ''
);
}
$azuracastEnv['COMPOSER_PLUGIN_MODE'] = $io->confirm(
$azuracastEnvConfig['COMPOSER_PLUGIN_MODE']['name'],
$azuracastEnv->getAsBool('COMPOSER_PLUGIN_MODE', false)

View File

@ -33,9 +33,9 @@ class EnvFile extends AbstractEnvFile
'required' => true,
],
'AZURACAST_VERSION' => [
'name' => __('Release Channel'),
'options' => ['latest', 'stable'],
'default' => 'latest',
'name' => __('Release Channel'),
'options' => ['latest', 'stable'],
'default' => 'latest',
'required' => true,
],
'AZURACAST_HTTP_PORT' => [
@ -84,20 +84,6 @@ class EnvFile extends AbstractEnvFile
'name' => __('Advanced: Use Privileged Docker Settings'),
'default' => true,
],
'LETSENCRYPT_HOST' => [
'name' => __('LetsEncrypt Domain Name(s)'),
'default' => '',
'description' => __(
'Domain name (example.com) or names (example.com,foo.bar) to use with LetsEncrypt.'
),
],
'LETSENCRYPT_EMAIL' => [
'name' => __('LetsEncrypt E-mail Address'),
'default' => '',
'description' => __(
'Optionally provide an e-mail address for updates from LetsEncrypt.',
),
],
];
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Message;
class GenerateAcmeCertificate extends AbstractMessage
{
/** @var string|null The path to log output of the Backup command to. */
public ?string $outputPath = null;
}

View File

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Radio;
class Certificate
{
public function __construct(
protected string $keyPath,
protected string $certPath
) {
}
public function getKeyPath(): string
{
return $this->keyPath;
}
public function getCertPath(): string
{
return $this->certPath;
}
}

View File

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Radio;
use App\Environment;
class CertificateLocator
{
public static function findCertificate(): Certificate
{
// Check environment variable for a virtual host.
$certBase = '/etc/nginx/certs';
if (is_dir($certBase)) {
if (!empty($_ENV['VIRTUAL_HOST'])) {
$vhost = $_ENV['VIRTUAL_HOST'];
$domainKey = $certBase . '/' . $vhost . '.key';
$domainCert = $certBase . '/' . $vhost . '.crt';
if (file_exists($domainKey) && file_exists($domainCert)) {
return new Certificate($domainKey, $domainCert);
}
}
$generatedKey = $certBase . '/ssl.key';
$generatedCert = $certBase . '/ssl.crt';
if (file_exists($generatedKey) && file_exists($generatedCert)) {
return new Certificate($generatedKey, $generatedCert);
}
$defaultKey = $certBase . '/default.key';
$defaultCert = $certBase . '/default.crt';
if (file_exists($defaultKey) && file_exists($defaultCert)) {
return new Certificate($defaultKey, $defaultCert);
}
}
return self::getDefaultCertificates();
}
public static function getDefaultCertificates(): Certificate
{
$environment = Environment::getInstance();
if ($environment->isDocker()) {
return new Certificate('/etc/nginx/ssl.key', '/etc/nginx/ssl.crt');
}
return new Certificate('/etc/nginx/ssl/server.key', '/etc/nginx/ssl/server.crt');
}
}

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Radio\Frontend;
use App\Entity;
use App\Radio\CertificateLocator;
use App\Radio\Enums\StreamFormats;
use App\Service\Acme;
use App\Utilities;
use App\Xml\Writer;
use Exception;
@ -114,7 +114,7 @@ class Icecast extends AbstractFrontend
$settingsBaseUrl = $settings->getBaseUrl() ?: '';
$baseUrl = Utilities\Urls::getUri($settingsBaseUrl) ?? new Uri('http://localhost');
$certPaths = CertificateLocator::findCertificate();
[$certPath, $certKey] = Acme::getCertificatePaths();
$config = [
'location' => 'AzuraCast',
@ -127,13 +127,13 @@ class Icecast extends AbstractFrontend
'client-timeout' => 30,
'header-timeout' => 15,
'source-timeout' => 10,
'burst-size' => 65535,
'burst-size' => 65535,
],
'authentication' => [
'source-password' => $frontendConfig->getSourcePassword(),
'relay-password' => $frontendConfig->getRelayPassword(),
'admin-user' => 'admin',
'admin-password' => $frontendConfig->getAdminPassword(),
'relay-password' => $frontendConfig->getRelayPassword(),
'admin-user' => 'admin',
'admin-password' => $frontendConfig->getAdminPassword(),
],
'listen-socket' => [
@ -154,8 +154,8 @@ class Icecast extends AbstractFrontend
'@dest' => '/status.xsl',
],
],
'ssl-private-key' => $certPaths->getKeyPath(),
'ssl-certificate' => $certPaths->getCertPath(),
'ssl-private-key' => $certKey,
'ssl-certificate' => $certPath,
// phpcs:disable Generic.Files.LineLength
'ssl-allowed-ciphers' => 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS',
// phpcs:enable
@ -232,9 +232,9 @@ class Icecast extends AbstractFrontend
$mountRelayUri = $mount_row->getRelayUrlAsUri();
if (null !== $mountRelayUri) {
$config['relay'][] = [
'server' => $mountRelayUri->getHost(),
'port' => $mountRelayUri->getPort(),
'mount' => $mountRelayUri->getPath(),
'server' => $mountRelayUri->getHost(),
'port' => $mountRelayUri->getPort(),
'mount' => $mountRelayUri->getPath(),
'local-mount' => $mount_row->getName(),
];
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Radio\Frontend;
use App\Entity;
use App\Radio\CertificateLocator;
use App\Service\Acme;
use Exception;
use NowPlaying\Result\Result;
use Psr\Http\Message\UriInterface;
@ -111,7 +111,7 @@ class Shoutcast extends AbstractFrontend
$configPath = $station->getRadioConfigDir();
$frontendConfig = $station->getFrontendConfig();
$certPaths = CertificateLocator::findCertificate();
[$certPath, $certKey] = Acme::getCertificatePaths();
$config = [
'password' => $frontendConfig->getSourcePassword(),
@ -128,8 +128,8 @@ class Shoutcast extends AbstractFrontend
'saveagentlistonexit' => '0',
'licenceid' => $frontendConfig->getScLicenseId(),
'userid' => $frontendConfig->getScUserId(),
'sslCertificateFile' => $certPaths->getCertPath(),
'sslCertificateKeyFile' => $certPaths->getKeyPath(),
'sslCertificateFile' => $certPath,
'sslCertificateKeyFile' => $certKey,
];
$customConfig = trim($frontendConfig->getCustomConfiguration() ?? '');
@ -168,7 +168,7 @@ class Shoutcast extends AbstractFrontend
$configFileOutput = '';
foreach ($config as $config_key => $config_value) {
$configFileOutput .= $config_key . '=' . str_replace("\n", '', (string) $config_value) . "\n";
$configFileOutput .= $config_key . '=' . str_replace("\n", '', (string)$config_value) . "\n";
}
return $configFileOutput;

206
src/Service/Acme.php Normal file
View File

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Repository\SettingsRepository;
use App\Entity\Repository\StationRepository;
use App\Environment;
use App\Message\AbstractMessage;
use App\Message\GenerateAcmeCertificate;
use App\Nginx\Nginx;
use App\Radio\Adapters;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LogLevel;
use skoerfgen\ACMECert\ACMECert;
use Symfony\Component\Filesystem\Filesystem;
final class Acme
{
public const LETSENCRYPT_PROD = 'https://acme-v02.api.letsencrypt.org/directory';
public const LETSENCRYPT_DEV = 'https://acme-staging-v02.api.letsencrypt.org/directory';
public const THRESHOLD_DAYS = 14;
public function __construct(
private readonly SettingsRepository $settingsRepo,
private readonly StationRepository $stationRepo,
private readonly Environment $environment,
private readonly Logger $logger,
private readonly Nginx $nginx,
private readonly Adapters $adapters,
) {
}
public function __invoke(AbstractMessage $message): void
{
if ($message instanceof GenerateAcmeCertificate) {
$outputPath = $message->outputPath;
if (null !== $outputPath) {
$logHandler = new StreamHandler($outputPath, LogLevel::DEBUG, true);
$this->logger->pushHandler($logHandler);
}
try {
$this->getCertificate();
} catch (\Exception $e) {
$this->logger->error(
sprintf('ACME Error: %s', $e->getMessage()),
[
'exception' => $e,
]
);
}
if (null !== $outputPath) {
$this->logger->popHandler();
}
}
}
public function getCertificate(bool $force = false): void
{
// Check folder permissions.
$acmeDir = self::getAcmeDirectory();
$fs = new Filesystem();
// Build ACME Cert class.
$directoryUrl = $this->environment->isProduction() ? self::LETSENCRYPT_PROD : self::LETSENCRYPT_DEV;
$this->logger->debug(
sprintf('ACME: Using directory URL: %s', $directoryUrl)
);
$acme = new ACMECert($directoryUrl);
// Build LetsEncrypt settings.
$settings = $this->settingsRepo->readSettings();
$acmeEmail = $settings->getAcmeEmail();
$acmeDomain = $settings->getAcmeDomains();
if (empty($acmeEmail)) {
$acmeEmail = getenv('LETSENCRYPT_EMAIL');
}
if (empty($acmeDomain)) {
$acmeDomain = getenv('LETSENCRYPT_HOST');
}
if (empty($acmeDomain)) {
$acmeDomain = $settings->getBaseUrlAsUri()?->getHost();
}
if (empty($acmeEmail) || empty($acmeDomain)) {
throw new \RuntimeException('Missing e-mail address or domain(s).');
}
$settings->setAcmeEmail($acmeEmail);
$settings->setAcmeDomains($acmeDomain);
$this->settingsRepo->writeSettings($settings);
// Account certificate registration.
if (file_exists($acmeDir . '/account_key.pem')) {
$acme->loadAccountKey('file://' . $acmeDir . '/account_key.pem');
} else {
$accountKey = $acme->generateECKey('P-384');
$fs->dumpFile($acmeDir . '/account_key.pem', $accountKey);
$acme->loadAccountKey($accountKey);
$acme->register(true, $acmeEmail);
}
// Renewal check.
if (
!$force
&& file_exists($acmeDir . '/acme.crt')
&& $acme->getRemainingDays('file://' . $acmeDir . '/acme.crt') > self::THRESHOLD_DAYS
) {
throw new \RuntimeException('Certificate does not need renewal.');
}
$fs->mkdir($acmeDir . '/challenges');
$domainConfig = [];
foreach (explode(',', $acmeDomain) as $domain) {
$domain = trim($domain);
$domainConfig[$domain] = ['challenge' => 'http-01'];
}
$handler = function ($opts) use ($acmeDir, $fs) {
$fs->dumpFile(
$acmeDir . '/challenges/' . basename($opts['key']),
$opts['value']
);
return function ($opts) use ($acmeDir, $fs) {
$fs->remove($acmeDir . '/challenges/' . $opts['key']);
};
};
if (!file_exists($acmeDir . '/acme.key')) {
$acmeKey = $acme->generateECKey('P-384');
$fs->dumpFile($acmeDir . '/acme.key', $acmeKey);
}
$fullchain = $acme->getCertificateChain(
'file://' . $acmeDir . '/acme.key',
$domainConfig,
$handler
);
$fs->dumpFile($acmeDir . '/acme.crt', $fullchain);
// Symlink to the shared SSL cert.
$fs->remove([
$acmeDir . '/ssl.crt',
$acmeDir . '/ssl.key',
]);
$fs->symlink($acmeDir . '/acme.crt', $acmeDir . '/ssl.crt');
$fs->symlink($acmeDir . '/acme.key', $acmeDir . '/ssl.key');
$this->reloadServices();
$this->logger->notice('ACME certificate process successful.');
}
private function reloadServices(): void
{
try {
$this->nginx->reload();
foreach ($this->stationRepo->iterateEnabledStations() as $station) {
if (!$station->getHasStarted()) {
continue;
}
$frontend = $this->adapters->getFrontendAdapter($station);
if ($frontend->supportsReload() && $frontend->isRunning($station)) {
$frontend->reload($station);
}
}
} catch (\Exception $e) {
$this->logger->error(
sprintf('ACME: Could not reload all adapters: %s', $e->getMessage()),
[
'exception' => $e,
]
);
}
}
public static function getAcmeDirectory(): string
{
return Environment::getInstance()->getParentDirectory() . '/acme';
}
public static function getCertificatePaths(): array
{
$acmeDir = self::getAcmeDirectory();
return [
$acmeDir . '/ssl.crt',
$acmeDir . '/ssl.key',
];
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Service\Acme;
use Psr\Log\LoggerInterface;
final class RenewAcmeCertTask extends AbstractTask
{
public function __construct(
ReloadableEntityManagerInterface $em,
LoggerInterface $logger,
private readonly Acme $acme
) {
parent::__construct($em, $logger);
}
public static function getSchedulePattern(): string
{
return '3 */6 * * *';
}
public function run(bool $force = false): void
{
try {
$this->acme->getCertificate();
} catch (\Exception $e) {
$this->logger->warning(
sprintf('ACME Failed: %s', $e->getMessage()),
[
'exception' => $e,
]
);
}
}
}

View File

@ -34,7 +34,7 @@ else
fi
APP_ENV="${APP_ENV:-production}"
UPDATE_REVISION="${UPDATE_REVISION:-87}"
UPDATE_REVISION="${UPDATE_REVISION:-88}"
echo "Updating AzuraCast (Environment: $APP_ENV, Update revision: $UPDATE_REVISION)"

View File

@ -27,11 +27,11 @@
- beanstalkd
- sftpgo
- mariadb
- azuracast-db-install
- ufw
- dbip
- composer
- services
- azuracast-db-install
- azuracast-build
- azuracast-setup
- azuracast-cron

View File

@ -48,5 +48,6 @@
- "{{ app_base }}/servers/icecast2"
- "{{ app_base }}/servers/stereo_tool"
- "{{ app_base }}/uploads"
- "{{ app_base }}/acme/challenges"
loop_control:
loop_var: azuracast_config_sys_directory

View File

@ -15,7 +15,7 @@
become_user: azuracast
shell: >-
php {{ www_base }}/bin/console azuracast:setup
when: update_mode|bool
when: !update_mode|bool
- name: Migrate Legacy Configuration (Update Mode)
become: true

View File

@ -33,21 +33,6 @@
- nginx-common
- libnginx-mod-nchan
- name: Create nginx ssl directory
file:
path: "/etc/nginx/ssl"
state: directory
owner: root
group: root
mode: 0744
- name: Create self-signed SSL cert
command: >-
openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=${ansible_fqdn}" -days 3650
-keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/server.crt -extensions v3_ca
args:
creates: /etc/nginx/ssl/server.crt
- name: Remove default nginx site symlink
file:
path: "/etc/nginx/sites-enabled/default"
@ -81,6 +66,25 @@
replace: 'sendfile off;'
when: app_env == "development"
- name: Create self-signed SSL cert
command: >-
openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=${ansible_fqdn}" -days 3650
-keyout {{ app_base }}/acme/default.key -out {{ app_base }}/acme/default.crt -extensions v3_ca
args:
creates: "{{ app_base }}/acme/default.crt"
- name: Link self-signed SSL key if applicable.
file:
path: "{{ app_base }}/acme/ssl.key"
state: link
src: "{{ app_base }}/acme/default.key"
- name: Link self-signed SSL cert if applicable.
file:
path: "{{ app_base }}/acme/ssl.crt"
state: link
src: "{{ app_base }}/acme/default.crt"
- name: Install Nginx Supervisord conf
template:
src: supervisor.conf.j2

View File

@ -58,8 +58,8 @@ server {
listen [::]:80;
listen [::]:443 default_server ssl;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_certificate {{ app_base }}/acme/ssl.crt;
ssl_certificate_key {{ app_base }}/acme/ssl.key;
root {{ app_base }}/www/web;
index index.php;
@ -73,6 +73,12 @@ server {
access_log {{ app_base }}/www_tmp/access.log;
error_log {{ app_base }}/www_tmp/error.log;
# LetsEncrypt handling
location /.well-known/acme-challenge {
alias {{ app_base }}/acme/challenges;
try_files $uri =404;
}
# Serve a static version of the nowplaying data for non-PHP-blocking delivery.
location /api/nowplaying_static {
expires 10s;

View File

@ -28,7 +28,7 @@
when: update_revision|int < 87
- role: "nginx"
when: update_revision|int < 87
when: update_revision|int < 88
- role: "redis"
when: update_revision|int < 87

View File

@ -13,7 +13,8 @@ usermod -aG www-data azuracast
mkdir -p /var/azuracast/www /var/azuracast/stations /var/azuracast/servers/shoutcast2 \
/var/azuracast/servers/stereo_tool /var/azuracast/backups /var/azuracast/www_tmp \
/var/azuracast/uploads /var/azuracast/geoip /var/azuracast/dbip
/var/azuracast/uploads /var/azuracast/geoip /var/azuracast/dbip \
/var/azuracast/acme
chown -R azuracast:azuracast /var/azuracast
chmod -R 777 /var/azuracast/www_tmp

View File

@ -53,13 +53,8 @@ server {
listen 80;
listen 443 default_server http2 ssl;
{{if exists "/etc/nginx/certs/ssl.crt"}}
ssl_certificate /etc/nginx/certs/ssl.crt;
ssl_certificate_key /etc/nginx/certs/ssl.key;
{{else}}
ssl_certificate /etc/nginx/certs/default.crt;
ssl_certificate_key /etc/nginx/certs/default.key;
{{end}}
ssl_certificate /var/azuracast/acme/ssl.crt;
ssl_certificate_key /var/azuracast/acme/ssl.key;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;
@ -79,8 +74,8 @@ server {
add_header Referrer-Policy no-referrer-when-downgrade;
# LetsEncrypt handling
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
location /.well-known/acme-challenge {
alias /var/azuracast/acme/challenges;
try_files $uri =404;
}

View File

@ -1,337 +0,0 @@
#!/bin/bash
# Acme loading script
# Uses code from:
# https://github.com/nginx-proxy/acme-companion/blob/main/app/letsencrypt_service
# We set a "LOG_LEVEL" that is incompatible with acme.sh. Overwrite it.
export LOG_LEVEL=1
export DEBUG=1
shopt -s expand_aliases
. /usr/local/acme.sh/acme.sh.env
function set_ownership_and_permissions {
local path="${1:?}"
# The default ownership is root:root, with 755 permissions for folders and 644 for files.
local user="azuracast"
local group="azuracast"
local f_perms="644"
local d_perms="755"
[[ "$DEBUG" == 1 ]] && echo "Debug: checking $path ownership and permissions."
# Find the user numeric ID if the FILES_UID environment variable isn't numeric.
if [[ "$user" =~ ^[0-9]+$ ]]; then
user_num="$user"
# Check if this user exist inside the container
elif id -u "$user" > /dev/null 2>&1; then
# Convert the user name to numeric ID
local user_num; user_num="$(id -u "$user")"
[[ "$DEBUG" == 1 ]] && echo "Debug: numeric ID of user $user is $user_num."
else
echo "Warning: user $user not found in the container, please use a numeric user ID instead of a user name. Skipping ownership and permissions check."
return 1
fi
# Find the group numeric ID if the FILES_GID environment variable isn't numeric.
if [[ "$group" =~ ^[0-9]+$ ]]; then
group_num="$group"
# Check if this group exist inside the container
elif getent group "$group" > /dev/null 2>&1; then
# Convert the group name to numeric ID
local group_num; group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
[[ "$DEBUG" == 1 ]] && echo "Debug: numeric ID of group $group is $group_num."
else
echo "Warning: group $group not found in the container, please use a numeric group ID instead of a group name. Skipping ownership and permissions check."
return 1
fi
# Check and modify ownership if required.
if [[ -e "$path" ]]; then
if [[ "$(stat -c %u:%g "$path" )" != "$user_num:$group_num" ]]; then
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path ownership to $user:$group."
if [[ -L "$path" ]]; then
chown -h "$user_num:$group_num" "$path"
else
chown "$user_num:$group_num" "$path"
fi
fi
# If the path is a folder, check and modify permissions if required.
if [[ -d "$path" ]]; then
if [[ "$(stat -c %a "$path")" != "$d_perms" ]]; then
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to $d_perms."
chmod "$d_perms" "$path"
fi
# If the path is a file, check and modify permissions if required.
elif [[ -f "$path" ]]; then
# Use different permissions for private files (private keys and ACME account files) ...
if [[ "$path" =~ ^.*(default\.key|key\.pem|\.json)$ ]]; then
if [[ "$(stat -c %a "$path")" != "$f_perms" ]]; then
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to $f_perms."
chmod "$f_perms" "$path"
fi
# ... and for public files (certificates, chains, fullchains, DH parameters).
else
if [[ "$(stat -c %a "$path")" != "644" ]]; then
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to 644."
chmod "644" "$path"
fi
fi
fi
else
echo "Warning: $path does not exist. Skipping ownership and permissions check."
return 1
fi
}
# Convert argument to lowercase (bash 4 only)
function lc() {
echo "${@,,}"
}
function create_link {
local -r source=${1?missing source argument}
local -r target=${2?missing target argument}
if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
set_ownership_and_permissions "$target"
[[ "$DEBUG" == 1 ]] && echo "$target already linked to $source"
return 1
else
ln -sf "$source" "$target" \
&& set_ownership_and_permissions "$target"
fi
}
function create_links {
local -r base_domain=${1?missing base_domain argument}
if [[ ! -f "/etc/nginx/certs/$base_domain/fullchain.pem" || \
! -f "/etc/nginx/certs/$base_domain/key.pem" ]]; then
return 1
fi
local return_code=1
create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/ssl.crt"
return_code=$(( return_code & $? ))
create_link "./$base_domain/key.pem" "/etc/nginx/certs/ssl.key"
return_code=$(( return_code & $? ))
if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
create_link ./dhparam.pem "/etc/nginx/certs/ssl.dhparam.pem"
return_code=$(( return_code & $? ))
fi
if [[ -f "/etc/nginx/certs/$base_domain/chain.pem" ]]; then
create_link "./$base_domain/chain.pem" "/etc/nginx/certs/ssl.chain.pem"
return_code=$(( return_code & $? ))
fi
return $return_code
}
CERTS_UPDATE_INTERVAL="${CERTS_UPDATE_INTERVAL:-3600}"
ACME_CA_URI="${ACME_CA_URI:-"https://acme-v02.api.letsencrypt.org/directory"}"
ACME_CA_TEST_URI="https://acme-staging-v02.api.letsencrypt.org/directory"
DEFAULT_KEY_SIZE="${DEFAULT_KEY_SIZE:-4096}"
RENEW_PRIVATE_KEYS="$(lc "${RENEW_PRIVATE_KEYS:-true}")"
# Backward compatibility environment variable
REUSE_PRIVATE_KEYS="$(lc "${REUSE_PRIVATE_KEYS:-false}")"
function update_cert {
local hosts_array
IFS=',' read -ra hosts_array <<< "$LETSENCRYPT_HOST"
local base_domain="${hosts_array[0]}"
# Base CLI parameters array, used for both --register-account and --issue
local -a params_base_arr
params_base_arr+=(--log /dev/null)
[[ "$DEBUG" == 1 ]] && params_base_arr+=(--debug 2)
# Alternative trusted root CA path, used for test with Pebble
if [[ -n "${CA_BUNDLE// }" ]]; then
if [[ -f "$CA_BUNDLE" ]]; then
params_base_arr+=(--ca-bundle "$CA_BUNDLE")
[[ "$DEBUG" == 1 ]] && echo "Debug: acme.sh will use $CA_BUNDLE as trusted root CA."
else
echo "Warning: the path to the alternate CA bundle ($CA_BUNDLE) is not valid, using default Alpine trust store."
fi
fi
# CLI parameters array used for --register-account
local -a params_register_arr
# CLI parameters array used for --issue
local -a params_issue_arr
params_issue_arr+=(--webroot /usr/share/nginx/html)
local -n cert_keysize="LETSENCRYPT_KEYSIZE"
if [[ -z "$cert_keysize" ]] || \
[[ ! "$cert_keysize" =~ ^(2048|3072|4096|ec-256|ec-384)$ ]]; then
cert_keysize=$DEFAULT_KEY_SIZE
fi
params_issue_arr+=(--keylength "$cert_keysize")
# OCSP-Must-Staple extension
local -n ocsp="ACME_OCSP"
if [[ $(lc "$ocsp") == true ]]; then
params_issue_arr+=(--ocsp-must-staple)
fi
local -n accountemail="LETSENCRYPT_EMAIL"
local config_home
# If we don't have a LETSENCRYPT_EMAIL from the proxied container
# and DEFAULT_EMAIL is set to a non empty value, use the latter.
if [[ -z "$accountemail" ]]; then
if [[ -n "${DEFAULT_EMAIL// }" ]]; then
accountemail="$DEFAULT_EMAIL"
else
unset accountemail
fi
fi
if [[ -n "${accountemail// }" ]]; then
# If we got an email, use it with the corresponding config home
config_home="/etc/acme.sh/$accountemail"
else
# If we did not get any email at all, use the default (empty mail) config
config_home="/etc/acme.sh/default"
fi
local -n acme_ca_uri="ACME_CA_URI"
if [[ -z "$acme_ca_uri" ]]; then
# Use default or user provided ACME end point
acme_ca_uri="$ACME_CA_URI"
fi
# LETSENCRYPT_TEST overrides LETSENCRYPT_ACME_CA_URI
local -n test_certificate="LETSENCRYPT_TEST"
if [[ $(lc "$test_certificate") == true ]]; then
# Use Let's Encrypt ACME V2 staging end point
acme_ca_uri="$ACME_CA_TEST_URI"
fi
# Set relevant --server parameter and ca folder name
params_base_arr+=(--server "$acme_ca_uri")
local ca_dir="${acme_ca_uri##*://}" \
&& ca_dir="${ca_dir%%:*}"
local certificate_dir
# If we're going to use one of LE stating endpoints ...
if [[ "$acme_ca_uri" =~ ^https://acme-staging.* ]]; then
# Unset accountemail
# force config dir to 'staging'
unset accountemail
config_home="/etc/acme.sh/staging"
# Prefix test certificate directory with _test_
certificate_dir="/etc/nginx/certs/_test_$base_domain"
else
certificate_dir="/etc/nginx/certs/$base_domain"
fi
params_issue_arr+=( \
--cert-file "${certificate_dir}/cert.pem" \
--key-file "${certificate_dir}/key.pem" \
--ca-file "${certificate_dir}/chain.pem" \
--fullchain-file "${certificate_dir}/fullchain.pem" \
)
[[ ! -d "$config_home" ]] && mkdir -p "$config_home"
params_base_arr+=(--config-home "$config_home")
local account_file="${config_home}/ca/${ca_dir}/account.json"
if [[ -n "${accountemail// }" ]]; then
# We're not using Zero SSL, register the ACME account using the provided email.
params_register_arr+=(--accountemail "$accountemail")
fi
# Account registration and update if required
if [[ ! -f "$account_file" ]]; then
params_register_arr=("${params_base_arr[@]}" "${params_register_arr[@]}")
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --register-account with the following parameters : ${params_register_arr[*]}"
acme.sh --register-account "${params_register_arr[@]}"
fi
if [[ -n "${accountemail// }" ]] && ! grep -q "mailto:$accountemail" "$account_file"; then
local -a params_update_arr=("${params_base_arr[@]}" --accountemail "$accountemail")
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --update-account with the following parameters : ${params_update_arr[*]}"
acme.sh --update-account "${params_update_arr[@]}"
fi
# If we still don't have an account.json file by this point, we've got an issue
if [[ ! -f "$account_file" ]]; then
echo "Error: no ACME account was found or registered for $accountemail and $acme_ca_uri, certificate creation aborted."
return 1
fi
local -n acme_preferred_chain="ACME_PREFERRED_CHAIN"
if [[ -n "${acme_preferred_chain}" ]]; then
# Using amce.sh --preferred-chain to select alternate chain.
params_issue_arr+=(--preferred-chain "$acme_preferred_chain")
fi
if [[ "$RENEW_PRIVATE_KEYS" != 'false' && "$REUSE_PRIVATE_KEYS" != 'true' ]]; then
params_issue_arr+=(--always-force-new-domain-key)
fi
[[ "${2:-}" == "--force-renew" ]] && params_issue_arr+=(--force)
# Create directory for the first domain
mkdir -p "$certificate_dir"
set_ownership_and_permissions "$certificate_dir"
for domain in "${hosts_array[@]}"; do
# Add all the domains to certificate
params_issue_arr+=(--domain "$domain")
done
params_issue_arr=("${params_base_arr[@]}" "${params_issue_arr[@]}")
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --issue with the following parameters : ${params_issue_arr[*]}"
echo "Creating/renewal $base_domain certificates... (${hosts_array[*]})"
acme.sh --issue "${params_issue_arr[@]}"
local acmesh_return=$?
local should_reload_nginx='false'
# 0 = success, 2 = RENEW_SKIP
if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
if [[ $acme_ca_uri =~ ^https://acme-staging.* ]]; then
create_links "_test_$base_domain" \
&& should_reload_nginx='true'
else
create_links "$base_domain" \
&& should_reload_nginx='true'
fi
# Make private key root readable only
for file in cert.pem key.pem chain.pem fullchain.pem; do
local file_path="${certificate_dir}/${file}"
[[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
done
[[ $acmesh_return -eq 0 ]] \
&& should_reload_nginx='true'
fi
if [[ "$should_reload_nginx" == 'true' ]]; then
echo "Reloading nginx..."
on_ssl_renewal
fi
}
if [ ! -z "$VIRTUAL_HOST" ]; then
echo "Multi-site configuration detected; skipping local ACME setup."
elif [ ! -z "$LETSENCRYPT_HOST" -a "$LETSENCRYPT_HOST" != " " ]; then
update_cert "$@"
fi
# Wait some amount of time
echo "Sleep for ${CERTS_UPDATE_INTERVAL}s"
sleep $CERTS_UPDATE_INTERVAL
exit

View File

@ -1,11 +0,0 @@
[program:acme]
command=run_acme_sh
priority=200
numprocs=1
autostart=true
autorestart=true
stdout_logfile=/proc/1/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/proc/1/fd/2
stderr_logfile_maxbytes=0

View File

@ -1,19 +0,0 @@
#!/bin/bash
set -e
set -x
# Get acme.sh ACME client source
mkdir /src
git -C /src clone https://github.com/acmesh-official/acme.sh.git
cd /src/acme.sh
# Install acme.sh in /app
./acme.sh --install \
--nocron \
--auto-upgrade 0 \
--home /usr/local/acme.sh \
--config-home /etc/acme.sh/default
# Make house cleaning
cd /
rm -rf /src

View File

@ -1,19 +1,26 @@
#!/bin/bash
if [ -f /etc/nginx/certs/default.crt ]; then
rm -rf /etc/nginx/certs/default.key || true
rm -rf /etc/nginx/certs/default.crt || true
mkdir -p /var/azuracast/acme/challenges || true
if [ -f /var/azuracast/acme/default.crt ]; then
rm -rf /var/azuracast/acme/default.key || true
rm -rf /var/azuracast/acme/default.crt || true
fi
# Generate a self-signed certificate if one doesn't exist in the certs path.
if [ ! -f /etc/nginx/certs/default.crt ]; then
if [ ! -f /var/azuracast/acme/default.crt ]; then
echo "Generating self-signed certificate..."
openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=localhost" \
-days 365 -extensions v3_ca \
-keyout /etc/nginx/certs/default.key \
-out /etc/nginx/certs/default.crt
-keyout /var/azuracast/acme/default.key \
-out /var/azuracast/acme/default.crt
fi
chown azuracast:azuracast /etc/nginx/certs/default.* || true
chmod 644 /etc/nginx/certs/default.* || true
if [ ! -f /var/azuracast/acme/ssl.crt ]; then
ln -s /var/azuracast/acme/default.key /var/azuracast/acme/ssl.key
ln -s /var/azuracast/acme/default.crt /var/azuracast/acme/ssl.crt
fi
chown -R azuracast:azuracast /var/azuracast/acme || true
chmod -R u=rwX,go=rX /var/azuracast/acme || true