diff --git a/dev/config.yml b/dev/config.yml index ca86c7613..ef9c234c3 100644 --- a/dev/config.yml +++ b/dev/config.yml @@ -15,7 +15,17 @@ playout: liquidsoap: server_listen_address: 0.0.0.0 + harbor_ssl_certificate: /certs/fake.crt + harbor_ssl_private_key: /certs/fake.key + stream: + inputs: + main: + public_url: https://localhost:8001/main + mount: main + port: 8001 + secure: true + outputs: .default_icecast_output: &default_icecast_output host: icecast diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 1862056c8..d6d33d62b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -32,6 +32,7 @@ services: volumes: - ./playout:/src - ./dev/playout:/app + - ./dev/certs:/certs ## See https://libretime.org/docs/admin-manual/tutorials/setup-a-pulseaudio-output-inside-containers/ # - ./dev/pulse.socket:/tmp/pulse.socket # - ./docker/pulse.client.conf:/etc/pulse/client.conf diff --git a/docker/config.template.yml b/docker/config.template.yml index 5948a6824..6f9938291 100644 --- a/docker/config.template.yml +++ b/docker/config.template.yml @@ -107,6 +107,13 @@ liquidsoap: # > default is ["0.0.0.0"] harbor_listen_address: ["0.0.0.0"] + # Input harbor tls certificate path. + harbor_ssl_certificate: + # Input harbor tls certificate private key path. + harbor_ssl_private_key: + # Input harbor tls certificate password. + harbor_ssl_password: + stream: # Inputs sources. inputs: @@ -121,6 +128,9 @@ stream: # Listen port for the main harbor input. # > default is 8001 port: 8001 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Show harbor input. show: @@ -133,6 +143,9 @@ stream: # Listen port for the show harbor input. # > default is 8002 port: 8002 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Output streams. outputs: diff --git a/docker/config.yml b/docker/config.yml index 524bc98ab..b6a11e3d5 100644 --- a/docker/config.yml +++ b/docker/config.yml @@ -107,6 +107,13 @@ liquidsoap: # > default is ["0.0.0.0"] harbor_listen_address: ["0.0.0.0"] + # Input harbor tls certificate path. + harbor_ssl_certificate: + # Input harbor tls certificate private key path. + harbor_ssl_private_key: + # Input harbor tls certificate password. + harbor_ssl_password: + stream: # Inputs sources. inputs: @@ -121,6 +128,9 @@ stream: # Listen port for the main harbor input. # > default is 8001 port: 8001 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Show harbor input. show: @@ -133,6 +143,9 @@ stream: # Listen port for the show harbor input. # > default is 8002 port: 8002 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Output streams. outputs: diff --git a/docker/example/config.yml b/docker/example/config.yml index 84044be67..0c601ea5f 100644 --- a/docker/example/config.yml +++ b/docker/example/config.yml @@ -107,6 +107,13 @@ liquidsoap: # > default is ["0.0.0.0"] harbor_listen_address: ["0.0.0.0"] + # Input harbor tls certificate path. + harbor_ssl_certificate: + # Input harbor tls certificate private key path. + harbor_ssl_private_key: + # Input harbor tls certificate password. + harbor_ssl_password: + stream: # Inputs sources. inputs: @@ -121,6 +128,9 @@ stream: # Listen port for the main harbor input. # > default is 8001 port: 8001 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Show harbor input. show: @@ -133,6 +143,9 @@ stream: # Listen port for the show harbor input. # > default is 8002 port: 8002 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Output streams. outputs: diff --git a/docs/admin-manual/configuration.md b/docs/admin-manual/configuration.md index 99f6befeb..d3273de3b 100644 --- a/docs/admin-manual/configuration.md +++ b/docs/admin-manual/configuration.md @@ -210,6 +210,13 @@ liquidsoap: # Input harbor listen address. # > default is ["0.0.0.0"] harbor_listen_address: ["0.0.0.0"] + + # Input harbor tls certificate path. + harbor_ssl_certificate: + # Input harbor tls certificate private key path. + harbor_ssl_private_key: + # Input harbor tls certificate password. + harbor_ssl_password: ``` ## Stream @@ -275,6 +282,9 @@ stream: # Listen port for the main harbor input. # > default is 8001 port: 8001 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Show harbor input. show: @@ -287,6 +297,9 @@ stream: # Listen port for the show harbor input. # > default is 8002 port: 8002 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false ``` ### Outputs diff --git a/docs/admin-manual/install/install-using-the-installer.md b/docs/admin-manual/install/install-using-the-installer.md index b2a3145fa..69a6b9508 100644 --- a/docs/admin-manual/install/install-using-the-installer.md +++ b/docs/admin-manual/install/install-using-the-installer.md @@ -360,6 +360,44 @@ Check that the renewal configuration is valid: sudo certbot renew --dry-run ``` +### Setup the certificate for Liquidsoap + +To stream audio content from an external source to the LibreTime server, Liquidsoap creates input harbors (Icecast mount points) for the clients to connect to. These mount points are insecure by default, so it's recommended secure them. + +To enable the secure input streams, edit the [configuration file](../configuration.md) at `/etc/libretime/config.yml` with the following, be sure to replace `libretime.example.com` with the domain name of your installation: + +```git title="/etc/libretime/config.yml" + liquidsoap: +- harbor_ssl_certificate: +- harbor_ssl_private_key: ++ harbor_ssl_certificate: /etc/letsencrypt/live/libretime.example.com/fullchain.pem ++ harbor_ssl_private_key: /etc/letsencrypt/live/libretime.example.com/privkey.pem +``` + +```git title="/etc/libretime/config.yml" + stream: + inputs: + main: + public_url: + mount: main + port: 8001 +- secure: false ++ secure: true + + show: + public_url: + mount: show + port: 8002 +- secure: false ++ secure: true +``` + +Restart the LibreTime to apply the changes: + +```bash +sudo systemctl restart libretime.target +``` + ## First login Once the setup is completed, log in the interface (with the default user `admin` and password `admin`), and edit the project settings (go to **Settings** > **General**) to match your needs. diff --git a/installer/config.yml b/installer/config.yml index 21eb33904..9f08555a2 100644 --- a/installer/config.yml +++ b/installer/config.yml @@ -107,6 +107,13 @@ liquidsoap: # > default is ["0.0.0.0"] harbor_listen_address: ["0.0.0.0"] + # Input harbor tls certificate path. + harbor_ssl_certificate: + # Input harbor tls certificate private key path. + harbor_ssl_private_key: + # Input harbor tls certificate password. + harbor_ssl_password: + stream: # Inputs sources. inputs: @@ -121,6 +128,9 @@ stream: # Listen port for the main harbor input. # > default is 8001 port: 8001 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Show harbor input. show: @@ -133,6 +143,9 @@ stream: # Listen port for the show harbor input. # > default is 8002 port: 8002 + # Whether the input harbor is secured with the tls certificate. + # > default is false + secure: false # Output streams. outputs: diff --git a/legacy/application/configs/conf.php b/legacy/application/configs/conf.php index 7630d6646..ed0e781c8 100644 --- a/legacy/application/configs/conf.php +++ b/legacy/application/configs/conf.php @@ -117,6 +117,7 @@ class Schema implements ConfigurationInterface /* */->validate()->ifString()->then($trim_leading_slash)->end() /* */->end() /* */->integerNode('port')->defaultValue(8001)->end() + /* */->booleanNode('secure')->defaultValue(False)->end() /**/->end()->end() /**/->arrayNode('show')->addDefaultsIfNotSet()->children() /* */->booleanNode('enabled')->defaultTrue()->end() @@ -126,6 +127,7 @@ class Schema implements ConfigurationInterface /* */->validate()->ifString()->then($trim_leading_slash)->end() /* */->end() /* */->integerNode('port')->defaultValue(8002)->end() + /* */->booleanNode('secure')->defaultValue(False)->end() /**/->end()->end() ->end()->end() diff --git a/legacy/application/models/Preference.php b/legacy/application/models/Preference.php index bb4ad7537..a3232addf 100644 --- a/legacy/application/models/Preference.php +++ b/legacy/application/models/Preference.php @@ -1090,8 +1090,11 @@ class Application_Model_Preference $host = Config::get('general.public_url_raw')->getHost(); $port = Application_Model_StreamSetting::getMasterLiveStreamPort(); $mount = Application_Model_StreamSetting::getMasterLiveStreamMountPoint(); + $secure = Application_Model_StreamSetting::getMasterLiveStreamSecure(); - return "http://{$host}:{$port}/{$mount}"; + $scheme = $secure ? 'https' : 'http'; + + return "{$scheme}://{$host}:{$port}/{$mount}"; } public static function GetLiveDJSourceConnectionURL() @@ -1103,8 +1106,11 @@ class Application_Model_Preference $host = Config::get('general.public_url_raw')->getHost(); $port = Application_Model_StreamSetting::getDjLiveStreamPort(); $mount = Application_Model_StreamSetting::getDjLiveStreamMountPoint(); + $secure = Application_Model_StreamSetting::getDjLiveStreamSecure(); - return "http://{$host}:{$port}/{$mount}"; + $scheme = $secure ? 'https' : 'http'; + + return "{$scheme}://{$host}:{$port}/{$mount}"; } public static function SetAutoTransition($value) diff --git a/legacy/application/models/StreamSetting.php b/legacy/application/models/StreamSetting.php index d8b0472c2..718154e08 100644 --- a/legacy/application/models/StreamSetting.php +++ b/legacy/application/models/StreamSetting.php @@ -190,6 +190,11 @@ class Application_Model_StreamSetting return Config::get('stream.inputs.main.mount') ?? 'main'; } + public static function getMasterLiveStreamSecure() + { + return Config::get('stream.inputs.main.secure') ?? false; + } + public static function getDjLiveStreamPort() { return Config::get('stream.inputs.show.port') ?? 8002; @@ -200,6 +205,11 @@ class Application_Model_StreamSetting return Config::get('stream.inputs.show.mount') ?? 'show'; } + public static function getDjLiveStreamSecure() + { + return Config::get('stream.inputs.show.secure') ?? false; + } + public static function getAdminUser($stream) { return self::getStreamDataNormalized($stream)['admin_user']; diff --git a/playout/libretime_playout/config.py b/playout/libretime_playout/config.py index 5c506e41d..2690c4240 100644 --- a/playout/libretime_playout/config.py +++ b/playout/libretime_playout/config.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Literal +from typing import List, Literal, Optional from libretime_shared.config import ( BaseConfig, @@ -7,7 +7,7 @@ from libretime_shared.config import ( RabbitMQConfig, StreamConfig, ) -from pydantic import BaseModel +from pydantic import BaseModel, root_validator CACHE_DIR = Path.cwd() / "scheduler" RECORD_DIR = Path.cwd() / "recorder" @@ -33,6 +33,23 @@ class LiquidsoapConfig(BaseModel): harbor_listen_address: List[str] = ["0.0.0.0"] + harbor_ssl_certificate: Optional[str] = None + harbor_ssl_private_key: Optional[str] = None + harbor_ssl_password: Optional[str] = None + + @root_validator + @classmethod + def _validate_harbor_ssl(cls, values: dict): + harbor_ssl_certificate = values.get("harbor_ssl_certificate") + harbor_ssl_private_key = values.get("harbor_ssl_private_key") + if harbor_ssl_certificate is not None and harbor_ssl_private_key is None: + raise ValueError("missing 'harbor_ssl_private_key' value") + + if harbor_ssl_certificate is None and harbor_ssl_private_key is not None: + raise ValueError("missing 'harbor_ssl_certificate' value") + + return values + class Config(BaseConfig): general: GeneralConfig diff --git a/playout/libretime_playout/liquidsoap/1.4/ls_script.liq b/playout/libretime_playout/liquidsoap/1.4/ls_script.liq index 6a244b83e..c7a481703 100644 --- a/playout/libretime_playout/liquidsoap/1.4/ls_script.liq +++ b/playout/libretime_playout/liquidsoap/1.4/ls_script.liq @@ -120,6 +120,14 @@ def input_main_on_disconnect() update_source_status("master_dj", false) end def input_show_on_connect(header) update_source_status("live_dj", true) end def input_show_on_disconnect() update_source_status("live_dj", false) end +def make_input_func(secure) + if secure then + input.harbor.ssl + else + input.harbor + end +end + def make_input_auth_handler(input_name) def auth_handler(user, password) log("user '#{user}' connected", label="#{input_name}_input") @@ -144,9 +152,10 @@ s = switch(id="switch:blank+schedule", ) s = if input_show_port != 0 and input_show_mount != "" then + input_show_func = make_input_func(input_show_secure) input_show_source = audio_to_stereo( - input.harbor(id="harbor:input_show", + input_show_func(id="harbor:input_show", input_show_mount, port=input_show_port, auth=make_input_auth_handler("show"), @@ -166,9 +175,10 @@ else end s = if input_main_port != 0 and input_main_mount != "" then + input_main_func = make_input_func(input_main_secure) input_main_source = audio_to_stereo( - input.harbor(id="harbor:input_main", + input_main_func(id="harbor:input_main", input_main_mount, port=input_main_port, auth=make_input_auth_handler("main"), diff --git a/playout/libretime_playout/liquidsoap/templates/entrypoint.liq.j2 b/playout/libretime_playout/liquidsoap/templates/entrypoint.liq.j2 index 0d6bab27f..c3d885e63 100644 --- a/playout/libretime_playout/liquidsoap/templates/entrypoint.liq.j2 +++ b/playout/libretime_playout/liquidsoap/templates/entrypoint.liq.j2 @@ -5,8 +5,10 @@ # Inputs input_main_mount = {{ config.stream.inputs.main.mount | quote }} input_main_port = {{ config.stream.inputs.main.port }} +input_main_secure = {{ config.stream.inputs.main.secure | string | lower }} input_show_mount = {{ config.stream.inputs.show.mount | quote }} input_show_port = {{ config.stream.inputs.show.port }} +input_show_secure = {{ config.stream.inputs.show.secure | string | lower }} # Settings {% if paths.log_filepath is defined -%} @@ -21,6 +23,14 @@ set("server.telnet.port", {{ config.liquidsoap.server_listen_port }}) set("harbor.bind_addrs", ["{{ config.liquidsoap.harbor_listen_address | join('", "') }}"]) +{% if config.liquidsoap.harbor_ssl_certificate -%} +set("harbor.ssl.certificate", "{{ config.liquidsoap.harbor_ssl_certificate }}") +set("harbor.ssl.private_key", "{{ config.liquidsoap.harbor_ssl_private_key }}") +{% if config.liquidsoap.harbor_ssl_password -%} +set("harbor.ssl.password", "{{ config.liquidsoap.harbor_ssl_password }}") +{%- endif %} +{% endif -%} + station_name = interactive.string("station_name", {{ info.station_name | quote }}) message_offline = interactive.string("message_offline", {{ preferences.message_offline | quote }}) diff --git a/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr b/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr index 342873863..4735502f4 100644 --- a/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr +++ b/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr @@ -8,8 +8,10 @@ # Inputs input_main_mount = "main" input_main_port = 8001 + input_main_secure = false input_show_mount = "show" input_show_port = 8002 + input_show_secure = false # Settings set("log.file.path", "/var/log/radio.log") @@ -43,8 +45,50 @@ # Inputs input_main_mount = "main" input_main_port = 8001 + input_main_secure = false input_show_mount = "show" input_show_port = 8002 + input_show_secure = false + + # Settings + set("log.file.path", "/var/log/radio.log") + + set("server.telnet", true) + set("server.telnet.bind_addr", "127.0.0.1") + set("server.telnet.port", 1234) + + set("harbor.bind_addrs", ["0.0.0.0"]) + + set("harbor.ssl.certificate", "/fake/ssl.cert") + set("harbor.ssl.private_key", "/fake/ssl.key") + + station_name = interactive.string("station_name", "LibreTime") + + message_offline = interactive.string("message_offline", "LibreTime - offline") + message_format = interactive.string("message_format", "0") + input_fade_transition = interactive.float("input_fade_transition", 0.0) + + %include "/fake/1.4/ls_script.liq" + + + + gateway("started") + + ''' +# --- +# name: test_generate_entrypoint[stream_config2-1.4] + ''' + # THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT! + ########################################################### + # The ignore() lines are to squash unused variable warnings + + # Inputs + input_main_mount = "main" + input_main_port = 8001 + input_main_secure = false + input_show_mount = "show" + input_show_port = 8002 + input_show_secure = false # Settings set("log.file.path", "/var/log/radio.log") @@ -94,7 +138,7 @@ ''' # --- -# name: test_generate_entrypoint[stream_config2-1.4] +# name: test_generate_entrypoint[stream_config3-1.4] ''' # THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT! ########################################################### @@ -103,8 +147,10 @@ # Inputs input_main_mount = "main" input_main_port = 8001 + input_main_secure = false input_show_mount = "show" input_show_port = 8002 + input_show_secure = false # Settings set("log.file.path", "/var/log/radio.log") @@ -150,7 +196,7 @@ ''' # --- -# name: test_generate_entrypoint[stream_config3-1.4] +# name: test_generate_entrypoint[stream_config4-1.4] ''' # THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT! ########################################################### @@ -159,8 +205,10 @@ # Inputs input_main_mount = "main" input_main_port = 8001 + input_main_secure = false input_show_mount = "show" input_show_port = 8002 + input_show_secure = false # Settings set("log.file.path", "/var/log/radio.log") @@ -231,7 +279,7 @@ ''' # --- -# name: test_generate_entrypoint[stream_config4-1.4] +# name: test_generate_entrypoint[stream_config5-1.4] ''' # THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT! ########################################################### @@ -240,8 +288,10 @@ # Inputs input_main_mount = "main" input_main_port = 8001 + input_main_secure = false input_show_mount = "show" input_show_port = 8002 + input_show_secure = false # Settings set("log.file.path", "/var/log/radio.log") @@ -274,7 +324,7 @@ ''' # --- -# name: test_generate_entrypoint[stream_config5-1.4] +# name: test_generate_entrypoint[stream_config6-1.4] ''' # THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT! ########################################################### @@ -283,8 +333,10 @@ # Inputs input_main_mount = "main" input_main_port = 8001 + input_main_secure = false input_show_mount = "show" input_show_port = 8002 + input_show_secure = false # Settings set("log.file.path", "/var/log/radio.log") diff --git a/playout/tests/liquidsoap/fixtures/__init__.py b/playout/tests/liquidsoap/fixtures/__init__.py index 407feb418..1675bbbb3 100644 --- a/playout/tests/liquidsoap/fixtures/__init__.py +++ b/playout/tests/liquidsoap/fixtures/__init__.py @@ -3,20 +3,33 @@ from typing import List from libretime_playout.config import Config -def make_config_with_stream(**kwargs) -> Config: +def make_config(**kwargs) -> Config: return Config( **{ "general": { "public_url": "http://localhost:8080", "api_key": "some_api_key", }, - "stream": kwargs, + **kwargs, } ) +def make_config_with_stream(**kwargs) -> Config: + return make_config(stream=kwargs) + + TEST_STREAM_CONFIGS: List[Config] = [ - make_config_with_stream(), + make_config(), + make_config( + liquidsoap={ + "harbor_ssl_certificate": "/fake/ssl.cert", + "harbor_ssl_private_key": "/fake/ssl.key", + }, + stream={ + "system": [{"enabled": True, "kind": "pulseaudio"}], + }, + ), make_config_with_stream( outputs={ "icecast": [ diff --git a/shared/libretime_shared/config/_models.py b/shared/libretime_shared/config/_models.py index bbfdd495f..094d763c3 100644 --- a/shared/libretime_shared/config/_models.py +++ b/shared/libretime_shared/config/_models.py @@ -133,6 +133,7 @@ class HarborInput(BaseInput): kind: Literal[InputKind.HARBOR] = InputKind.HARBOR mount: str port: int + secure: bool = False _mount_no_leading_slash = no_leading_slash_validator("mount")