Merge commit 'aab4a0c7b5edab1ca261a528e3407ff2f1b59297'
This commit is contained in:
parent
18fe4060e7
commit
1e3ccd93d5
|
@ -5,7 +5,9 @@ release channel, you can take advantage of these new features and fixes.
|
||||||
|
|
||||||
## New Features/Changes
|
## 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
|
## Code Quality/Technical Changes
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"users":[],"groups":[],"folders":[],"admins":[],"api_keys":[],"shares":[],"version":12}
|
|
@ -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}
|
|
@ -70,6 +70,7 @@
|
||||||
"psr/simple-cache": ">1",
|
"psr/simple-cache": ">1",
|
||||||
"ramsey/uuid": "^4.0",
|
"ramsey/uuid": "^4.0",
|
||||||
"rlanvin/php-ip": "dev-master",
|
"rlanvin/php-ip": "dev-master",
|
||||||
|
"skoerfgen/acmecert": "^3.2",
|
||||||
"slim/http": "^1.1",
|
"slim/http": "^1.1",
|
||||||
"slim/slim": "^4.2",
|
"slim/slim": "^4.2",
|
||||||
"spatie/flysystem-dropbox": "^2",
|
"spatie/flysystem-dropbox": "^2",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d0f53f37fc280407f5070076e53e3bc2",
|
"content-hash": "13899e60907b126bd3c5b7ea5cbf1278",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
@ -6242,6 +6242,50 @@
|
||||||
},
|
},
|
||||||
"time": "2022-03-02T08:51:37+00:00"
|
"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",
|
"name": "slim/http",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
|
@ -4,6 +4,7 @@ use App\Console\Command;
|
||||||
|
|
||||||
return function (App\Event\BuildConsoleCommands $event) {
|
return function (App\Event\BuildConsoleCommands $event) {
|
||||||
$event->addAliases([
|
$event->addAliases([
|
||||||
|
'azuracast:acme:get-certificate' => Command\Acme\GetCertificateCommand::class,
|
||||||
'azuracast:backup' => Command\Backup\BackupCommand::class,
|
'azuracast:backup' => Command\Backup\BackupCommand::class,
|
||||||
'azuracast:restore' => Command\Backup\RestoreCommand::class,
|
'azuracast:restore' => Command\Backup\RestoreCommand::class,
|
||||||
'azuracast:debug:optimize-tables' => Command\Debug\OptimizeTablesCommand::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:process' => Command\MessageQueue\ProcessCommand::class,
|
||||||
'queue:clear' => Command\MessageQueue\ClearCommand::class,
|
'queue:clear' => Command\MessageQueue\ClearCommand::class,
|
||||||
'cache:clear' => Command\ClearCacheCommand::class,
|
'cache:clear' => Command\ClearCacheCommand::class,
|
||||||
|
'acme:cert' => Command\Acme\GetCertificateCommand::class,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -138,6 +138,7 @@ return function (CallableEventDispatcherInterface $dispatcher) {
|
||||||
App\Sync\Task\MoveBroadcastsTask::class,
|
App\Sync\Task\MoveBroadcastsTask::class,
|
||||||
App\Sync\Task\QueueInterruptingTracks::class,
|
App\Sync\Task\QueueInterruptingTracks::class,
|
||||||
App\Sync\Task\ReactivateStreamerTask::class,
|
App\Sync\Task\ReactivateStreamerTask::class,
|
||||||
|
App\Sync\Task\RenewAcmeCertTask::class,
|
||||||
App\Sync\Task\RotateLogsTask::class,
|
App\Sync\Task\RotateLogsTask::class,
|
||||||
App\Sync\Task\RunAnalyticsTask::class,
|
App\Sync\Task\RunAnalyticsTask::class,
|
||||||
App\Sync\Task\RunAutomatedAssignmentTask::class,
|
App\Sync\Task\RunAutomatedAssignmentTask::class,
|
||||||
|
|
|
@ -13,6 +13,8 @@ return [
|
||||||
|
|
||||||
Message\BackupMessage::class => Task\RunBackupTask::class,
|
Message\BackupMessage::class => Task\RunBackupTask::class,
|
||||||
|
|
||||||
|
Message\GenerateAcmeCertificate::class => App\Service\Acme::class,
|
||||||
|
|
||||||
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
|
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||||
Message\TestWebhookMessage::class => App\Webhook\Dispatcher::class,
|
Message\TestWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,16 @@ return static function (RouteCollectorProxy $group) {
|
||||||
Controller\Api\Admin\SendTestMessageAction::class
|
Controller\Api\Admin\SendTestMessageAction::class
|
||||||
)->setName('api:admin:send-test-message');
|
)->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(
|
$group->get(
|
||||||
'/custom_assets/{type}',
|
'/custom_assets/{type}',
|
||||||
Controller\Api\Admin\CustomAssets\GetCustomAssetAction::class
|
Controller\Api\Admin\CustomAssets\GetCustomAssetAction::class
|
||||||
|
|
|
@ -10,7 +10,8 @@ services:
|
||||||
- "127.0.0.1:3306:3306"
|
- "127.0.0.1:3306:3306"
|
||||||
- "127.0.0.1:6379:6379"
|
- "127.0.0.1:6379:6379"
|
||||||
volumes:
|
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/vendor:/var/azuracast/www/vendor
|
||||||
- $PWD:/var/azuracast/www
|
- $PWD:/var/azuracast/www
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
|
|
@ -183,8 +183,6 @@ services:
|
||||||
PUID: ${AZURACAST_PUID:-1000}
|
PUID: ${AZURACAST_PUID:-1000}
|
||||||
PGID: ${AZURACAST_PGID:-1000}
|
PGID: ${AZURACAST_PGID:-1000}
|
||||||
volumes:
|
volumes:
|
||||||
- letsencrypt:/etc/nginx/certs
|
|
||||||
- letsencrypt_acme:/etc/acme.sh
|
|
||||||
- www_uploads:/var/azuracast/uploads
|
- www_uploads:/var/azuracast/uploads
|
||||||
- station_data:/var/azuracast/stations
|
- station_data:/var/azuracast/stations
|
||||||
- shoutcast2_install:/var/azuracast/servers/shoutcast2
|
- shoutcast2_install:/var/azuracast/servers/shoutcast2
|
||||||
|
@ -192,6 +190,7 @@ services:
|
||||||
- geolite_install:/var/azuracast/geoip
|
- geolite_install:/var/azuracast/geoip
|
||||||
- sftpgo_data:/var/azuracast/sftpgo/persist
|
- sftpgo_data:/var/azuracast/sftpgo/persist
|
||||||
- backups:/var/azuracast/backups
|
- backups:/var/azuracast/backups
|
||||||
|
- acme:/var/azuracast/acme
|
||||||
- db_data:/var/lib/mysql
|
- db_data:/var/lib/mysql
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ulimits: &default-ulimits
|
ulimits: &default-ulimits
|
||||||
|
@ -205,8 +204,7 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data: { }
|
db_data: { }
|
||||||
letsencrypt: { }
|
acme: { }
|
||||||
letsencrypt_acme: { }
|
|
||||||
shoutcast2_install: { }
|
shoutcast2_install: { }
|
||||||
stereo_tool_install: { }
|
stereo_tool_install: { }
|
||||||
geolite_install: { }
|
geolite_install: { }
|
||||||
|
|
17
docker.sh
17
docker.sh
|
@ -200,14 +200,6 @@ setup-ports() {
|
||||||
envfile-set "AZURACAST_SFTP_PORT" "2022" "Port to use for SFTP connections"
|
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.
|
# Configure release mode settings.
|
||||||
#
|
#
|
||||||
|
@ -792,13 +784,14 @@ uninstall() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Create and link a LetsEncrypt SSL certificate.
|
# LetsEncrypt: Now managed via the Web UI.
|
||||||
# Usage: ./docker.sh letsencrypt-create
|
|
||||||
#
|
#
|
||||||
|
setup-letsencrypt() {
|
||||||
|
echo "LetsEncrypt is now managed from within the web interface."
|
||||||
|
}
|
||||||
|
|
||||||
letsencrypt-create() {
|
letsencrypt-create() {
|
||||||
setup-letsencrypt
|
setup-letsencrypt
|
||||||
docker-compose down
|
|
||||||
docker-compose up -d
|
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
:tab-class="getTabClass($v.securityPrivacyTab)"></settings-security-privacy-tab>
|
:tab-class="getTabClass($v.securityPrivacyTab)"></settings-security-privacy-tab>
|
||||||
<settings-services-tab :form="$v.form" :tab-class="getTabClass($v.servicesTab)"
|
<settings-services-tab :form="$v.form" :tab-class="getTabClass($v.servicesTab)"
|
||||||
:release-channel="releaseChannel"
|
:release-channel="releaseChannel"
|
||||||
:test-message-url="testMessageUrl"></settings-services-tab>
|
:test-message-url="testMessageUrl"
|
||||||
|
:acme-url="acmeUrl"></settings-services-tab>
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
</b-overlay>
|
</b-overlay>
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
apiUrl: String,
|
apiUrl: String,
|
||||||
testMessageUrl: String,
|
testMessageUrl: String,
|
||||||
|
acmeUrl: String,
|
||||||
releaseChannel: {
|
releaseChannel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'rolling',
|
default: 'rolling',
|
||||||
|
@ -84,6 +86,8 @@ export default {
|
||||||
api_access_control: {},
|
api_access_control: {},
|
||||||
|
|
||||||
check_for_updates: {},
|
check_for_updates: {},
|
||||||
|
acme_email: {},
|
||||||
|
acme_domains: {},
|
||||||
mail_enabled: {},
|
mail_enabled: {},
|
||||||
mail_sender_name: {},
|
mail_sender_name: {},
|
||||||
mail_sender_email: {},
|
mail_sender_email: {},
|
||||||
|
@ -106,7 +110,9 @@ export default {
|
||||||
'form.analytics', 'form.always_use_ssl', 'form.api_access_control'
|
'form.analytics', 'form.always_use_ssl', 'form.api_access_control'
|
||||||
],
|
],
|
||||||
servicesTab: [
|
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_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.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',
|
'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,
|
api_access_control: data.api_access_control,
|
||||||
|
|
||||||
check_for_updates: data.check_for_updates,
|
check_for_updates: data.check_for_updates,
|
||||||
|
acme_email: data.acme_email,
|
||||||
|
acme_domains: data.acme_domains,
|
||||||
mail_enabled: data.mail_enabled,
|
mail_enabled: data.mail_enabled,
|
||||||
mail_sender_name: data.mail_sender_name,
|
mail_sender_name: data.mail_sender_name,
|
||||||
mail_sender_email: data.mail_sender_email,
|
mail_sender_email: data.mail_sender_email,
|
||||||
|
|
|
@ -35,6 +35,49 @@
|
||||||
</b-form-row>
|
</b-form-row>
|
||||||
</b-form-fieldset>
|
</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>
|
<b-form-fieldset>
|
||||||
<template #label>
|
<template #label>
|
||||||
<translate key="lang_section_email_delivery">E-mail Delivery Service</translate>
|
<translate key="lang_section_email_delivery">E-mail Delivery Service</translate>
|
||||||
|
@ -177,6 +220,8 @@
|
||||||
</b-form-row>
|
</b-form-row>
|
||||||
</b-form-fieldset>
|
</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>
|
<admin-settings-test-message-modal :test-message-url="testMessageUrl"></admin-settings-test-message-modal>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
</template>
|
</template>
|
||||||
|
@ -188,18 +233,25 @@ import BFormFieldset from "~/components/Form/BFormFieldset";
|
||||||
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
|
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
|
||||||
import AdminSettingsTestMessageModal from "~/components/Admin/Settings/TestMessageModal";
|
import AdminSettingsTestMessageModal from "~/components/Admin/Settings/TestMessageModal";
|
||||||
import Icon from "~/components/Common/Icon";
|
import Icon from "~/components/Common/Icon";
|
||||||
|
import StreamingLogModal from "~/components/Common/StreamingLogModal";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingsServicesTab',
|
name: 'SettingsServicesTab',
|
||||||
components: {
|
components: {
|
||||||
|
StreamingLogModal,
|
||||||
Icon,
|
Icon,
|
||||||
AdminSettingsTestMessageModal, BWrappedFormCheckbox, BFormFieldset, BWrappedFormGroup, BFormMarkup
|
AdminSettingsTestMessageModal,
|
||||||
|
BWrappedFormCheckbox,
|
||||||
|
BFormFieldset,
|
||||||
|
BWrappedFormGroup,
|
||||||
|
BFormMarkup
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
form: Object,
|
form: Object,
|
||||||
tabClass: {},
|
tabClass: {},
|
||||||
releaseChannel: String,
|
releaseChannel: String,
|
||||||
testMessageUrl: String,
|
testMessageUrl: String,
|
||||||
|
acmeUrl: String,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
langTabTitle() {
|
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>
|
</script>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ namespace App\Console\Command;
|
||||||
|
|
||||||
use App\Entity;
|
use App\Entity;
|
||||||
use App\Environment;
|
use App\Environment;
|
||||||
|
use App\Service\Acme;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
@ -20,6 +21,7 @@ class InitializeCommand extends CommandAbstract
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Environment $environment,
|
protected Environment $environment,
|
||||||
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
|
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
|
||||||
|
protected Acme $acme,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
@ -68,6 +70,13 @@ class InitializeCommand extends CommandAbstract
|
||||||
// Ensure default storage locations exist.
|
// Ensure default storage locations exist.
|
||||||
$this->storageLocationRepo->createDefaultStorageLocations();
|
$this->storageLocationRepo->createDefaultStorageLocations();
|
||||||
|
|
||||||
|
// Pull Acme certificates if necessary.
|
||||||
|
try {
|
||||||
|
$this->acme->getCertificate();
|
||||||
|
} catch (\Exception) {
|
||||||
|
// Noop
|
||||||
|
}
|
||||||
|
|
||||||
$io->newLine();
|
$io->newLine();
|
||||||
$io->success(
|
$io->success(
|
||||||
[
|
[
|
||||||
|
|
|
@ -33,6 +33,7 @@ final class SettingsAction
|
||||||
'group' => Settings::GROUP_GENERAL,
|
'group' => Settings::GROUP_GENERAL,
|
||||||
]),
|
]),
|
||||||
'testMessageUrl' => (string)$router->named('api:admin:send-test-message'),
|
'testMessageUrl' => (string)$router->named('api:admin:send-test-message'),
|
||||||
|
'acmeUrl' => (string)$router->named('api:admin:acme'),
|
||||||
'releaseChannel' => $this->version->getReleaseChannelEnum()->value,
|
'releaseChannel' => $this->version->getReleaseChannelEnum()->value,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,18 @@ final class StationRepository extends Repository
|
||||||
return $select;
|
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
|
* @param string $short_code
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1006,6 +1006,40 @@ class Settings implements Stringable
|
||||||
$this->avatar_default_url = $avatarDefaultUrl;
|
$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
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return 'Settings';
|
return 'Settings';
|
||||||
|
|
|
@ -241,23 +241,6 @@ class InstallCommand extends Command
|
||||||
$env['AZURACAST_STATION_PORTS'] = implode(',', $stationPorts);
|
$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(
|
$azuracastEnv['COMPOSER_PLUGIN_MODE'] = $io->confirm(
|
||||||
$azuracastEnvConfig['COMPOSER_PLUGIN_MODE']['name'],
|
$azuracastEnvConfig['COMPOSER_PLUGIN_MODE']['name'],
|
||||||
$azuracastEnv->getAsBool('COMPOSER_PLUGIN_MODE', false)
|
$azuracastEnv->getAsBool('COMPOSER_PLUGIN_MODE', false)
|
||||||
|
|
|
@ -33,9 +33,9 @@ class EnvFile extends AbstractEnvFile
|
||||||
'required' => true,
|
'required' => true,
|
||||||
],
|
],
|
||||||
'AZURACAST_VERSION' => [
|
'AZURACAST_VERSION' => [
|
||||||
'name' => __('Release Channel'),
|
'name' => __('Release Channel'),
|
||||||
'options' => ['latest', 'stable'],
|
'options' => ['latest', 'stable'],
|
||||||
'default' => 'latest',
|
'default' => 'latest',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
],
|
],
|
||||||
'AZURACAST_HTTP_PORT' => [
|
'AZURACAST_HTTP_PORT' => [
|
||||||
|
@ -84,20 +84,6 @@ class EnvFile extends AbstractEnvFile
|
||||||
'name' => __('Advanced: Use Privileged Docker Settings'),
|
'name' => __('Advanced: Use Privileged Docker Settings'),
|
||||||
'default' => true,
|
'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.',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||||
namespace App\Radio\Frontend;
|
namespace App\Radio\Frontend;
|
||||||
|
|
||||||
use App\Entity;
|
use App\Entity;
|
||||||
use App\Radio\CertificateLocator;
|
|
||||||
use App\Radio\Enums\StreamFormats;
|
use App\Radio\Enums\StreamFormats;
|
||||||
|
use App\Service\Acme;
|
||||||
use App\Utilities;
|
use App\Utilities;
|
||||||
use App\Xml\Writer;
|
use App\Xml\Writer;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
@ -114,7 +114,7 @@ class Icecast extends AbstractFrontend
|
||||||
$settingsBaseUrl = $settings->getBaseUrl() ?: '';
|
$settingsBaseUrl = $settings->getBaseUrl() ?: '';
|
||||||
$baseUrl = Utilities\Urls::getUri($settingsBaseUrl) ?? new Uri('http://localhost');
|
$baseUrl = Utilities\Urls::getUri($settingsBaseUrl) ?? new Uri('http://localhost');
|
||||||
|
|
||||||
$certPaths = CertificateLocator::findCertificate();
|
[$certPath, $certKey] = Acme::getCertificatePaths();
|
||||||
|
|
||||||
$config = [
|
$config = [
|
||||||
'location' => 'AzuraCast',
|
'location' => 'AzuraCast',
|
||||||
|
@ -127,13 +127,13 @@ class Icecast extends AbstractFrontend
|
||||||
'client-timeout' => 30,
|
'client-timeout' => 30,
|
||||||
'header-timeout' => 15,
|
'header-timeout' => 15,
|
||||||
'source-timeout' => 10,
|
'source-timeout' => 10,
|
||||||
'burst-size' => 65535,
|
'burst-size' => 65535,
|
||||||
],
|
],
|
||||||
'authentication' => [
|
'authentication' => [
|
||||||
'source-password' => $frontendConfig->getSourcePassword(),
|
'source-password' => $frontendConfig->getSourcePassword(),
|
||||||
'relay-password' => $frontendConfig->getRelayPassword(),
|
'relay-password' => $frontendConfig->getRelayPassword(),
|
||||||
'admin-user' => 'admin',
|
'admin-user' => 'admin',
|
||||||
'admin-password' => $frontendConfig->getAdminPassword(),
|
'admin-password' => $frontendConfig->getAdminPassword(),
|
||||||
],
|
],
|
||||||
|
|
||||||
'listen-socket' => [
|
'listen-socket' => [
|
||||||
|
@ -154,8 +154,8 @@ class Icecast extends AbstractFrontend
|
||||||
'@dest' => '/status.xsl',
|
'@dest' => '/status.xsl',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'ssl-private-key' => $certPaths->getKeyPath(),
|
'ssl-private-key' => $certKey,
|
||||||
'ssl-certificate' => $certPaths->getCertPath(),
|
'ssl-certificate' => $certPath,
|
||||||
// phpcs:disable Generic.Files.LineLength
|
// 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',
|
'ssl-allowed-ciphers' => 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS',
|
||||||
// phpcs:enable
|
// phpcs:enable
|
||||||
|
@ -232,9 +232,9 @@ class Icecast extends AbstractFrontend
|
||||||
$mountRelayUri = $mount_row->getRelayUrlAsUri();
|
$mountRelayUri = $mount_row->getRelayUrlAsUri();
|
||||||
if (null !== $mountRelayUri) {
|
if (null !== $mountRelayUri) {
|
||||||
$config['relay'][] = [
|
$config['relay'][] = [
|
||||||
'server' => $mountRelayUri->getHost(),
|
'server' => $mountRelayUri->getHost(),
|
||||||
'port' => $mountRelayUri->getPort(),
|
'port' => $mountRelayUri->getPort(),
|
||||||
'mount' => $mountRelayUri->getPath(),
|
'mount' => $mountRelayUri->getPath(),
|
||||||
'local-mount' => $mount_row->getName(),
|
'local-mount' => $mount_row->getName(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||||
namespace App\Radio\Frontend;
|
namespace App\Radio\Frontend;
|
||||||
|
|
||||||
use App\Entity;
|
use App\Entity;
|
||||||
use App\Radio\CertificateLocator;
|
use App\Service\Acme;
|
||||||
use Exception;
|
use Exception;
|
||||||
use NowPlaying\Result\Result;
|
use NowPlaying\Result\Result;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
|
@ -111,7 +111,7 @@ class Shoutcast extends AbstractFrontend
|
||||||
$configPath = $station->getRadioConfigDir();
|
$configPath = $station->getRadioConfigDir();
|
||||||
$frontendConfig = $station->getFrontendConfig();
|
$frontendConfig = $station->getFrontendConfig();
|
||||||
|
|
||||||
$certPaths = CertificateLocator::findCertificate();
|
[$certPath, $certKey] = Acme::getCertificatePaths();
|
||||||
|
|
||||||
$config = [
|
$config = [
|
||||||
'password' => $frontendConfig->getSourcePassword(),
|
'password' => $frontendConfig->getSourcePassword(),
|
||||||
|
@ -128,8 +128,8 @@ class Shoutcast extends AbstractFrontend
|
||||||
'saveagentlistonexit' => '0',
|
'saveagentlistonexit' => '0',
|
||||||
'licenceid' => $frontendConfig->getScLicenseId(),
|
'licenceid' => $frontendConfig->getScLicenseId(),
|
||||||
'userid' => $frontendConfig->getScUserId(),
|
'userid' => $frontendConfig->getScUserId(),
|
||||||
'sslCertificateFile' => $certPaths->getCertPath(),
|
'sslCertificateFile' => $certPath,
|
||||||
'sslCertificateKeyFile' => $certPaths->getKeyPath(),
|
'sslCertificateKeyFile' => $certKey,
|
||||||
];
|
];
|
||||||
|
|
||||||
$customConfig = trim($frontendConfig->getCustomConfiguration() ?? '');
|
$customConfig = trim($frontendConfig->getCustomConfiguration() ?? '');
|
||||||
|
@ -168,7 +168,7 @@ class Shoutcast extends AbstractFrontend
|
||||||
|
|
||||||
$configFileOutput = '';
|
$configFileOutput = '';
|
||||||
foreach ($config as $config_key => $config_value) {
|
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;
|
return $configFileOutput;
|
||||||
|
|
|
@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
APP_ENV="${APP_ENV:-production}"
|
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)"
|
echo "Updating AzuraCast (Environment: $APP_ENV, Update revision: $UPDATE_REVISION)"
|
||||||
|
|
||||||
|
|
|
@ -27,11 +27,11 @@
|
||||||
- beanstalkd
|
- beanstalkd
|
||||||
- sftpgo
|
- sftpgo
|
||||||
- mariadb
|
- mariadb
|
||||||
- azuracast-db-install
|
|
||||||
- ufw
|
- ufw
|
||||||
- dbip
|
- dbip
|
||||||
- composer
|
- composer
|
||||||
- services
|
- services
|
||||||
|
- azuracast-db-install
|
||||||
- azuracast-build
|
- azuracast-build
|
||||||
- azuracast-setup
|
- azuracast-setup
|
||||||
- azuracast-cron
|
- azuracast-cron
|
||||||
|
|
|
@ -48,5 +48,6 @@
|
||||||
- "{{ app_base }}/servers/icecast2"
|
- "{{ app_base }}/servers/icecast2"
|
||||||
- "{{ app_base }}/servers/stereo_tool"
|
- "{{ app_base }}/servers/stereo_tool"
|
||||||
- "{{ app_base }}/uploads"
|
- "{{ app_base }}/uploads"
|
||||||
|
- "{{ app_base }}/acme/challenges"
|
||||||
loop_control:
|
loop_control:
|
||||||
loop_var: azuracast_config_sys_directory
|
loop_var: azuracast_config_sys_directory
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
become_user: azuracast
|
become_user: azuracast
|
||||||
shell: >-
|
shell: >-
|
||||||
php {{ www_base }}/bin/console azuracast:setup
|
php {{ www_base }}/bin/console azuracast:setup
|
||||||
when: update_mode|bool
|
when: !update_mode|bool
|
||||||
|
|
||||||
- name: Migrate Legacy Configuration (Update Mode)
|
- name: Migrate Legacy Configuration (Update Mode)
|
||||||
become: true
|
become: true
|
||||||
|
|
|
@ -33,21 +33,6 @@
|
||||||
- nginx-common
|
- nginx-common
|
||||||
- libnginx-mod-nchan
|
- 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
|
- name: Remove default nginx site symlink
|
||||||
file:
|
file:
|
||||||
path: "/etc/nginx/sites-enabled/default"
|
path: "/etc/nginx/sites-enabled/default"
|
||||||
|
@ -81,6 +66,25 @@
|
||||||
replace: 'sendfile off;'
|
replace: 'sendfile off;'
|
||||||
when: app_env == "development"
|
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
|
- name: Install Nginx Supervisord conf
|
||||||
template:
|
template:
|
||||||
src: supervisor.conf.j2
|
src: supervisor.conf.j2
|
||||||
|
|
|
@ -58,8 +58,8 @@ server {
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
listen [::]:443 default_server ssl;
|
listen [::]:443 default_server ssl;
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/ssl/server.crt;
|
ssl_certificate {{ app_base }}/acme/ssl.crt;
|
||||||
ssl_certificate_key /etc/nginx/ssl/server.key;
|
ssl_certificate_key {{ app_base }}/acme/ssl.key;
|
||||||
|
|
||||||
root {{ app_base }}/www/web;
|
root {{ app_base }}/www/web;
|
||||||
index index.php;
|
index index.php;
|
||||||
|
@ -73,6 +73,12 @@ server {
|
||||||
access_log {{ app_base }}/www_tmp/access.log;
|
access_log {{ app_base }}/www_tmp/access.log;
|
||||||
error_log {{ app_base }}/www_tmp/error.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.
|
# Serve a static version of the nowplaying data for non-PHP-blocking delivery.
|
||||||
location /api/nowplaying_static {
|
location /api/nowplaying_static {
|
||||||
expires 10s;
|
expires 10s;
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
when: update_revision|int < 87
|
when: update_revision|int < 87
|
||||||
|
|
||||||
- role: "nginx"
|
- role: "nginx"
|
||||||
when: update_revision|int < 87
|
when: update_revision|int < 88
|
||||||
|
|
||||||
- role: "redis"
|
- role: "redis"
|
||||||
when: update_revision|int < 87
|
when: update_revision|int < 87
|
||||||
|
|
|
@ -13,7 +13,8 @@ usermod -aG www-data azuracast
|
||||||
|
|
||||||
mkdir -p /var/azuracast/www /var/azuracast/stations /var/azuracast/servers/shoutcast2 \
|
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/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
|
chown -R azuracast:azuracast /var/azuracast
|
||||||
chmod -R 777 /var/azuracast/www_tmp
|
chmod -R 777 /var/azuracast/www_tmp
|
||||||
|
|
|
@ -53,13 +53,8 @@ server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen 443 default_server http2 ssl;
|
listen 443 default_server http2 ssl;
|
||||||
|
|
||||||
{{if exists "/etc/nginx/certs/ssl.crt"}}
|
ssl_certificate /var/azuracast/acme/ssl.crt;
|
||||||
ssl_certificate /etc/nginx/certs/ssl.crt;
|
ssl_certificate_key /var/azuracast/acme/ssl.key;
|
||||||
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_protocols TLSv1.3 TLSv1.2;
|
ssl_protocols TLSv1.3 TLSv1.2;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
@ -79,8 +74,8 @@ server {
|
||||||
add_header Referrer-Policy no-referrer-when-downgrade;
|
add_header Referrer-Policy no-referrer-when-downgrade;
|
||||||
|
|
||||||
# LetsEncrypt handling
|
# LetsEncrypt handling
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge {
|
||||||
root /usr/share/nginx/html;
|
alias /var/azuracast/acme/challenges;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,19 +1,26 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if [ -f /etc/nginx/certs/default.crt ]; then
|
mkdir -p /var/azuracast/acme/challenges || true
|
||||||
rm -rf /etc/nginx/certs/default.key || true
|
|
||||||
rm -rf /etc/nginx/certs/default.crt || 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
|
fi
|
||||||
|
|
||||||
# Generate a self-signed certificate if one doesn't exist in the certs path.
|
# 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..."
|
echo "Generating self-signed certificate..."
|
||||||
|
|
||||||
openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=localhost" \
|
openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=localhost" \
|
||||||
-days 365 -extensions v3_ca \
|
-days 365 -extensions v3_ca \
|
||||||
-keyout /etc/nginx/certs/default.key \
|
-keyout /var/azuracast/acme/default.key \
|
||||||
-out /etc/nginx/certs/default.crt
|
-out /var/azuracast/acme/default.crt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown azuracast:azuracast /etc/nginx/certs/default.* || true
|
if [ ! -f /var/azuracast/acme/ssl.crt ]; then
|
||||||
chmod 644 /etc/nginx/certs/default.* || true
|
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
|
||||||
|
|
Loading…
Reference in New Issue