diff --git a/actions/ejabberd b/actions/ejabberd index dd9565bcf..cc492fffa 100755 --- a/actions/ejabberd +++ b/actions/ejabberd @@ -7,20 +7,22 @@ Configuration helper for the ejabberd service import argparse import json import os -import pathlib +import re import shutil import socket import subprocess +import sys from distutils.version import LooseVersion as LV - -from ruamel.yaml import YAML, scalarstring +from pathlib import Path from plinth import action_utils +from ruamel.yaml import YAML, scalarstring EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml' EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump' EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump' EJABBERD_ORIG_CERT = '/etc/ejabberd/ejabberd.pem' +EJABBERD_MANAGED_COTURN = '/etc/ejabberd/freedombox_managed_coturn' IQDISC_DEPRECATED_VERSION = LV('18.03') MOD_IRC_DEPRECATED_VERSION = LV('18.06') @@ -70,6 +72,18 @@ def parse_arguments(): help='Add a domain name to ejabberd') add_domain.add_argument('--domainname', help='New domain name') + # Configure STUN/TURN server for use with ejabberd + turn = subparsers.add_parser( + 'configure-turn', help='Configure a TURN server for use with ejabberd') + turn.add_argument( + '--managed', required=False, default=False, action='store_true', + help='Whether configuration is provided by user or auto-managed by ' + 'FreedomBox') + + subparsers.add_parser( + 'get-turn-config', + help='Get the latest STUN/TURN configuration in JSON format') + # Switch/check Message Archive Management (MAM) in ejabberd config help_MAM = 'Switch or check Message Archive Management (MAM).' mam = subparsers.add_parser('mam', help=help_MAM) @@ -107,6 +121,8 @@ def subcommand_setup(arguments): for listen_port in conf['listen']: if 'tls' in listen_port: listen_port['tls'] = False + if 'use_turn' in listen_port: + conf['listen'].remove(listen_port) # Use coturn instead conf['auth_method'] = 'ldap' conf['ldap_servers'] = [scalarstring.DoubleQuotedScalarString('localhost')] @@ -154,7 +170,7 @@ def upgrade_config(domain): if listen_port['port'] == 5280: listen_port['port'] = 5443 - cert_dir = pathlib.Path('/etc/ejabberd/letsencrypt') / domain + cert_dir = Path('/etc/ejabberd/letsencrypt') / domain cert_file = str(cert_dir / 'ejabberd.pem') cert_file = scalarstring.DoubleQuotedScalarString(cert_file) conf['s2s_certfile'] = cert_file @@ -285,6 +301,81 @@ def subcommand_mam(argument): subprocess.call(['ejabberdctl', 'reload_config']) +def _generate_service(uri: str) -> dict: + """Generate ejabberd mod_stun_disco service config from Coturn URI.""" + pattern = re.compile(r'(stun|turn):(.*):([0-9]{4})\?transport=(tcp|udp)') + typ, domain, port, transport = pattern.match(uri).groups() + return { + "host": domain, + "port": int(port), + "type": typ, + "transport": transport, + "restricted": False + } + + +def _generate_uris(services: list[dict]) -> list[str]: + """Generate STUN/TURN URIs from ejabberd mod_stun_disco service config.""" + return [ + f"{s['type']}:{s['host']}:{s['port']}?transport={s['transport']}" + for s in services + ] + + +def subcommand_get_turn_config(_): + """Get the latest STUN/TURN configuration in JSON format.""" + with open(EJABBERD_CONFIG, 'r') as file_handle: + conf = yaml.load(file_handle) + + mod_stun_disco_config = conf['modules']['mod_stun_disco'] + managed = os.path.exists(EJABBERD_MANAGED_COTURN) + + if bool(mod_stun_disco_config): + print( + json.dumps([{ + 'domain': '', + 'uris': _generate_uris(mod_stun_disco_config['services']), + 'shared_secret': mod_stun_disco_config['secret'], + }, managed])) + else: + print( + json.dumps([{ + 'domain': None, + 'uris': [], + 'shared_secret': None + }, managed])) + + +def subcommand_configure_turn(arguments): + """Set parameters for the STUN/TURN server to use with ejabberd.""" + turn_server_config = json.loads(''.join(sys.stdin)) + uris = turn_server_config['uris'] + mod_stun_disco_config = {} + + if turn_server_config['uris'] and turn_server_config['shared_secret']: + mod_stun_disco_config = { + 'credentials_lifetime': '1000d', + 'secret': turn_server_config['shared_secret'], + 'services': [_generate_service(uri) for uri in uris] + } + + with open(EJABBERD_CONFIG, 'r') as file_handle: + conf = yaml.load(file_handle) + + conf['modules']['mod_stun_disco'] = mod_stun_disco_config + + with open(EJABBERD_CONFIG, 'w') as file_handle: + yaml.dump(conf, file_handle) + + if arguments.managed: + Path(EJABBERD_MANAGED_COTURN).touch() + else: + Path(EJABBERD_MANAGED_COTURN).unlink(missing_ok=True) + + if action_utils.service_is_running('ejabberd'): + subprocess.call(['ejabberdctl', 'reload_config']) + + def _get_version(): """ Get the current ejabberd version """ try: diff --git a/plinth/modules/coturn/components.py b/plinth/modules/coturn/components.py index b7f91a27f..22b9b7ad3 100644 --- a/plinth/modules/coturn/components.py +++ b/plinth/modules/coturn/components.py @@ -2,11 +2,10 @@ """App component for other apps to manage their STUN/TURN server configuration. """ -from __future__ import annotations +from __future__ import annotations # Can be removed in Python 3.10 import json from dataclasses import dataclass, field -from typing import List from plinth import app @@ -27,15 +26,15 @@ class TurnConfiguration: """ domain: str = None - uris: List[str] = field(default_factory=list) + uris: list[str] = field(default_factory=list) shared_secret: str = None def __post_init__(self): """Generate URIs after object initialization if necessary.""" if self.domain and not self.uris: self.uris = [ - f'{proto1}:{self.domain}:3478?transport={proto2}' - for proto1 in ['stun', 'turn'] for proto2 in ['tcp', 'udp'] + f'{typ}:{self.domain}:3478?transport={transport}' + for typ in ['stun', 'turn'] for transport in ['tcp', 'udp'] ] def to_json(self) -> str: @@ -73,7 +72,7 @@ class TurnConsumer(app.FollowerComponent): self._all[component_id] = self @classmethod - def list(cls) -> List[TurnConsumer]: # noqa + def list(cls) -> list[TurnConsumer]: # noqa """Return a list of all Coturn components.""" return cls._all.values() diff --git a/plinth/modules/ejabberd/__init__.py b/plinth/modules/ejabberd/__init__.py index 99f2f7daf..7676b686f 100644 --- a/plinth/modules/ejabberd/__init__.py +++ b/plinth/modules/ejabberd/__init__.py @@ -9,7 +9,6 @@ import pathlib from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ - from plinth import actions from plinth import app as app_module from plinth import cfg, frontpage, menu @@ -17,6 +16,7 @@ from plinth.daemon import Daemon from plinth.modules import config from plinth.modules.apache.components import Webserver from plinth.modules.backups.components import BackupRestore +from plinth.modules.coturn.components import TurnConfiguration, TurnConsumer from plinth.modules.firewall.components import Firewall from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.modules.users.components import UsersAndGroups @@ -26,7 +26,7 @@ from plinth.utils import format_lazy from . import manifest -version = 3 +version = 4 managed_services = ['ejabberd'] @@ -44,9 +44,15 @@ _description = [ 'When enabled, ejabberd can be accessed by any ' ' user with a {box_name} login.'), box_name=_(cfg.box_name), users_url=reverse_lazy('users:index'), - jsxc_url=reverse_lazy('jsxc:index')) + jsxc_url=reverse_lazy('jsxc:index')), + format_lazy( + _('ejabberd needs a STUN/TURN server for audio/video calls. ' + 'Install the Coturn app or configure ' + 'an external server.'), coturn_url=reverse_lazy('coturn:index')) ] +depends = ['coturn'] + logger = logging.getLogger(__name__) app = None @@ -113,11 +119,22 @@ class EjabberdApp(app_module.App): **manifest.backup) self.add(backup_restore) + turn = EjabberdTurnConsumer('turn-ejabberd') + self.add(turn) + pre_hostname_change.connect(on_pre_hostname_change) post_hostname_change.connect(on_post_hostname_change) domain_added.connect(on_domain_added) +class EjabberdTurnConsumer(TurnConsumer): + """Component to manage Coturn configuration for ejabberd.""" + + def on_config_change(self, config): + """Add or update STUN/TURN configuration.""" + update_turn_configuration(config) + + def setup(helper, old_version=None): """Install and configure the module.""" domainname = config.get_domainname() @@ -134,6 +151,10 @@ def setup(helper, old_version=None): ['setup', '--domainname', domainname]) helper.call('post', app.enable) + # Configure STUN/TURN only if there's a valid TLS domain set for Coturn + configuration = app.get_component('turn-ejabberd').get_configuration() + update_turn_configuration(configuration, force=True) + def get_domains(): """Return the list of domains that ejabberd is interested in. @@ -194,3 +215,23 @@ def on_domain_added(sender, domain_type, name, description='', services=None, if name not in conf['domains']: actions.superuser_run('ejabberd', ['add-domain', '--domainname', name]) app.get_component('letsencrypt-ejabberd').setup_certificates() + + +def update_turn_configuration(config: TurnConfiguration, managed=True, + force=False): + """Update ejabberd's STUN/TURN server configuration.""" + setup_helper = globals()['setup_helper'] + if not force and setup_helper.get_state() == 'needs-setup': + return + + params = ['configure-turn'] + params += ['--managed'] if managed else [] + actions.superuser_run('ejabberd', params, input=config.to_json().encode()) + + +def get_turn_configuration() -> (TurnConfiguration, bool): + """Get the latest STUN/TURN configuration.""" + json_config = actions.superuser_run('ejabberd', ['get-turn-config']) + tc, managed = json.loads(json_config) + return (TurnConfiguration(tc['domain'], tc['uris'], + tc['shared_secret']), managed) diff --git a/plinth/modules/ejabberd/forms.py b/plinth/modules/ejabberd/forms.py index d5faea6e1..ff0825ade 100644 --- a/plinth/modules/ejabberd/forms.py +++ b/plinth/modules/ejabberd/forms.py @@ -4,6 +4,7 @@ Forms for configuring Ejabberd. """ from django import forms +from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ from plinth import cfg @@ -20,3 +21,28 @@ class EjabberdForm(forms.Form): 'clients, and reading the history of a multi-user chat room. ' 'It depends on the client settings whether the histories are ' 'stored as plain text or encrypted.'), box_name=_(cfg.box_name))) + + enable_managed_turn = forms.BooleanField( + label=_('Automatically manage audio/video call setup'), required=False, + help_text=format_lazy( + _('Configures the local coturn app as ' + 'the STUN/TURN server for ejabberd. Disable this if you ' + 'want to use a different STUN/TURN server.'), + coturn_url=reverse_lazy('coturn:index'))) + + # STUN/TURN server setup + turn_uris = forms.CharField( + label=_('STUN/TURN Server URIs'), required=False, strip=True, + widget=forms.Textarea(attrs={'rows': 4}), + help_text=_('List of public URIs of the STUN/TURN server, one on each ' + 'line.')) + + shared_secret = forms.CharField( + label=_('Shared Authentication Secret'), required=False, strip=True, + help_text=_('Shared secret used to compute passwords for the ' + 'TURN server.')) + + def clean_turn_uris(self): + """Normalize newlines in URIs.""" + data = self.cleaned_data['turn_uris'] + return '\n'.join([uri.strip() for uri in data.splitlines()]) diff --git a/plinth/modules/ejabberd/static/ejabberd.js b/plinth/modules/ejabberd/static/ejabberd.js new file mode 100644 index 000000000..6822a474e --- /dev/null +++ b/plinth/modules/ejabberd/static/ejabberd.js @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/** + * @licstart The following is the entire license notice for the JavaScript + * code in this page. + * + * This file is part of FreedomBox. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * @licend The above is the entire license notice for the JavaScript code + * in this page. + */ + +jQuery(function($) { + $('#id_enable_managed_turn').change(function() { + if($(this).prop('checked')) { + $('#id_turn_uris').closest('.form-group').hide(); + $('#id_shared_secret').closest('.form-group').hide(); + } else { + $('#id_turn_uris').closest('.form-group').show(); + $('#id_shared_secret').closest('.form-group').show(); + } + }).change(); +}); diff --git a/plinth/modules/ejabberd/templates/ejabberd.html b/plinth/modules/ejabberd/templates/ejabberd.html index fcf3fba51..c220994fd 100644 --- a/plinth/modules/ejabberd/templates/ejabberd.html +++ b/plinth/modules/ejabberd/templates/ejabberd.html @@ -3,8 +3,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later {% endcomment %} -{% load i18n %} {% load bootstrap %} +{% load i18n %} +{% load static %} {% block description %} @@ -28,3 +29,8 @@

{% endblock %} + +{% block page_js %} + +{% endblock %} diff --git a/plinth/modules/ejabberd/tests/conftest.py b/plinth/modules/ejabberd/tests/conftest.py new file mode 100644 index 000000000..599814073 --- /dev/null +++ b/plinth/modules/ejabberd/tests/conftest.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Common test fixtures for ejabberd. +""" + +import importlib +import pathlib +import shutil +import types +from unittest.mock import MagicMock, patch + +import pytest +from plinth.modules import ejabberd + +current_directory = pathlib.Path(__file__).parent + + +def _load_actions_module(): + actions_file_path = str(current_directory / '..' / '..' / '..' / '..' / + 'actions' / 'ejabberd') + loader = importlib.machinery.SourceFileLoader('ejabberd', + actions_file_path) + module = types.ModuleType(loader.name) + loader.exec_module(module) + return module + + +actions = _load_actions_module() + + +@pytest.fixture(name='conf_file') +def fixture_conf_file(tmp_path): + """Uses a dummy configuration file.""" + settings_file_name = 'ejabberd.yml' + conf_file = tmp_path / settings_file_name + conf_file.touch() + shutil.copyfile(str(current_directory / 'data' / 'ejabberd.yml.example'), + str(conf_file)) + return str(conf_file) + + +@pytest.fixture(name='managed_file') +def fixture_managed_file(tmp_path): + """Uses a dummy managed file.""" + file_name = 'freedombox_managed_coturn' + fil = tmp_path / file_name + return str(fil) + + +@pytest.fixture(name='call_action') +def fixture_call_action(capsys, conf_file, managed_file): + """Run actions with custom root path.""" + + def _call_action(module_name, args, **kwargs): + actions.EJABBERD_CONFIG = conf_file + actions.EJABBERD_MANAGED_COTURN = managed_file + with patch('argparse._sys.argv', [module_name] + args): + actions.main() + captured = capsys.readouterr() + return captured.out + + return _call_action + + +@pytest.fixture(name='test_configuration', autouse=True) +def fixture_test_configuration(call_action, conf_file): + """Use a separate ejabberd configuration for tests. + + Patches actions.superuser_run with the fixture call_action. + The module state is patched to be 'up-to-date'. + """ + with patch('plinth.actions.superuser_run', call_action): + + helper = MagicMock() + helper.get_state.return_value = 'up-to-date' + ejabberd.setup_helper = helper + + yield diff --git a/plinth/modules/ejabberd/tests/data/ejabberd.yml.example b/plinth/modules/ejabberd/tests/data/ejabberd.yml.example new file mode 100644 index 000000000..c6f36dbd4 --- /dev/null +++ b/plinth/modules/ejabberd/tests/data/ejabberd.yml.example @@ -0,0 +1,231 @@ +### +### ejabberd configuration file +### +### The parameters used in this configuration file are explained at +### +### https://docs.ejabberd.im/admin/configuration +### +### The configuration file is written in YAML. +### ******************************************************* +### ******* !!! WARNING !!! ******* +### ******* YAML IS INDENTATION SENSITIVE ******* +### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY ******* +### ******************************************************* +### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. +### + +hosts: + - localhost + +loglevel: info + +## If you already have certificates, list them here +# certfiles: +# - /etc/letsencrypt/live/domain.tld/fullchain.pem +# - /etc/letsencrypt/live/domain.tld/privkey.pem + +listen: + - + port: 5222 + ip: "::" + module: ejabberd_c2s + max_stanza_size: 262144 + shaper: c2s_shaper + access: c2s + starttls_required: true + - + port: 5269 + ip: "::" + module: ejabberd_s2s_in + max_stanza_size: 524288 + - + port: 5443 + ip: "::" + module: ejabberd_http + tls: true + request_handlers: + /admin: ejabberd_web_admin + /api: mod_http_api + /bosh: mod_bosh + /captcha: ejabberd_captcha + /upload: mod_http_upload + /ws: ejabberd_http_ws + - + port: 5280 + ip: "::" + module: ejabberd_http + request_handlers: + /admin: ejabberd_web_admin + /.well-known/acme-challenge: ejabberd_acme + - + port: 3478 + ip: "::" + transport: udp + module: ejabberd_stun + use_turn: true + ## The server's public IPv4 address: + # turn_ipv4_address: "203.0.113.3" + ## The server's public IPv6 address: + # turn_ipv6_address: "2001:db8::3" + - + port: 1883 + ip: "::" + module: mod_mqtt + backlog: 1000 + +s2s_use_starttls: optional + +acl: + local: + user_regexp: "" + loopback: + ip: + - 127.0.0.0/8 + - ::1/128 + +access_rules: + local: + allow: local + c2s: + deny: blocked + allow: all + announce: + allow: admin + configure: + allow: admin + muc_create: + allow: local + pubsub_createnode: + allow: local + trusted_network: + allow: loopback + +api_permissions: + "console commands": + from: + - ejabberd_ctl + who: all + what: "*" + "admin access": + who: + access: + allow: + - acl: loopback + - acl: admin + oauth: + scope: "ejabberd:admin" + access: + allow: + - acl: loopback + - acl: admin + what: + - "*" + - "!stop" + - "!start" + "public commands": + who: + ip: 127.0.0.1/8 + what: + - status + - connected_users_number + +shaper: + normal: + rate: 3000 + burst_size: 20000 + fast: 100000 + +shaper_rules: + max_user_sessions: 10 + max_user_offline_messages: + 5000: admin + 100: all + c2s_shaper: + none: admin + normal: all + s2s_shaper: fast + +modules: + mod_adhoc: {} + mod_admin_extra: {} + mod_announce: + access: announce + mod_avatar: {} + mod_blocking: {} + mod_bosh: {} + mod_caps: {} + mod_carboncopy: {} + mod_client_state: {} + mod_configure: {} + mod_disco: {} + mod_fail2ban: {} + mod_http_api: {} + mod_http_upload: + put_url: https://@HOST@:5443/upload + custom_headers: + "Access-Control-Allow-Origin": "https://@HOST@" + "Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS" + "Access-Control-Allow-Headers": "Content-Type" + mod_last: {} + mod_mam: + ## Mnesia is limited to 2GB, better to use an SQL backend + ## For small servers SQLite is a good fit and is very easy + ## to configure. Uncomment this when you have SQL configured: + ## db_type: sql + assume_mam_usage: true + default: always + mod_mqtt: {} + mod_muc: + access: + - allow + access_admin: + - allow: admin + access_create: muc_create + access_persistent: muc_create + access_mam: + - allow + default_room_options: + mam: true + mod_muc_admin: {} + mod_offline: + access_max_user_messages: max_user_offline_messages + mod_ping: {} + mod_privacy: {} + mod_private: {} + mod_proxy65: + access: local + max_connections: 5 + mod_pubsub: + access_createnode: pubsub_createnode + plugins: + - flat + - pep + force_node_config: + ## Avoid buggy clients to make their bookmarks public + storage:bookmarks: + access_model: whitelist + mod_push: {} + mod_push_keepalive: {} + mod_register: + ## Only accept registration requests from the "trusted" + ## network (see access_rules section above). + ## Think twice before enabling registration from any + ## address. See the Jabber SPAM Manifesto for details: + ## https://github.com/ge0rg/jabber-spam-fighting-manifesto + ip_access: trusted_network + mod_roster: + versioning: true + mod_s2s_dialback: {} + mod_shared_roster: {} + mod_stream_mgmt: + resend_on_timeout: if_offline + mod_stun_disco: {} + mod_vcard: {} + mod_vcard_xupdate: {} + mod_version: + show_os: false + +### Local Variables: +### mode: yaml +### End: +### vim: set filetype=yaml tabstop=8 diff --git a/plinth/modules/ejabberd/tests/test_turn_config.py b/plinth/modules/ejabberd/tests/test_turn_config.py new file mode 100644 index 000000000..a27171388 --- /dev/null +++ b/plinth/modules/ejabberd/tests/test_turn_config.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Test module for ejabberd STUN/TURN configuration. +""" + +from unittest.mock import patch + +from plinth.modules import ejabberd +from plinth.modules.coturn.components import TurnConfiguration + +managed_configuration = TurnConfiguration( + 'freedombox.local', [], + 'aiP02OAGyOlj6WGuCyqj7iaOsbkC7BUeKvKzhAsTZ8MEwMd3yTwpr2uvbOxgWe51') + +overridden_configuration = TurnConfiguration( + 'public.coturn.site', [], + 'BUeKvKzhAsTZ8MEwMd3yTwpr2uvbOxgWe51aiP02OAGyOlj6WGuCyqj7iaOsbkC7') + + +def _set_turn_configuration(monkeypatch, config=managed_configuration, + managed=True): + monkeypatch.setattr('sys.stdin', config.to_json()) + with patch('plinth.action_utils.service_is_running', return_value=False): + ejabberd.update_turn_configuration(config, managed=managed) + + +def _assert_conf(expected_configuration, expected_managed): + """Assert that ejabberd TURN configuration is as expected.""" + config, managed = ejabberd.get_turn_configuration() + assert config.uris == expected_configuration.uris + assert config.shared_secret == expected_configuration.shared_secret + assert managed == expected_managed + + +def test_managed_turn_server_configuration(monkeypatch): + _set_turn_configuration(monkeypatch) + _assert_conf(managed_configuration, True) + + +def test_overridden_turn_server_configuration(monkeypatch): + _set_turn_configuration(monkeypatch, overridden_configuration, False) + _assert_conf(overridden_configuration, False) + + +def test_remove_turn_configuration(monkeypatch): + _set_turn_configuration(monkeypatch) + _set_turn_configuration(monkeypatch, TurnConfiguration(), False) + _assert_conf(TurnConfiguration(), False) diff --git a/plinth/modules/ejabberd/views.py b/plinth/modules/ejabberd/views.py index d96dbf271..ad6f99bcd 100644 --- a/plinth/modules/ejabberd/views.py +++ b/plinth/modules/ejabberd/views.py @@ -3,11 +3,12 @@ Views for the Ejabberd module """ +from plinth.modules.coturn.components import TurnConfiguration from django.contrib import messages from django.utils.translation import ugettext as _ from plinth import actions -from plinth.modules import ejabberd +from plinth.modules import coturn, ejabberd from plinth.views import AppView from .forms import EjabberdForm @@ -20,9 +21,14 @@ class EjabberdAppView(AppView): form_class = EjabberdForm def get_initial(self): - initdict = super().get_initial() - initdict.update({'MAM_enabled': self.is_MAM_enabled()}) - return initdict + """Initial data to fill in the form.""" + config, managed = ejabberd.get_turn_configuration() + return super().get_initial() | { + 'MAM_enabled': self.is_MAM_enabled(), + 'enable_managed_turn': managed, + 'turn_uris': '\n'.join(config.uris), + 'shared_secret': config.shared_secret + } def get_context_data(self, *args, **kwargs): """Add service to the context data.""" @@ -31,21 +37,52 @@ class EjabberdAppView(AppView): context['domainname'] = domains[0] if domains else None return context + @staticmethod + def _handle_turn_configuration(old_config, new_config): + if not new_config['enable_managed_turn']: + new_turn_uris = new_config['turn_uris'].splitlines() + new_shared_secret = new_config['shared_secret'] + + turn_config_changed = \ + old_config['turn_uris'] != new_turn_uris or \ + old_config['shared_secret'] != new_shared_secret + + if turn_config_changed: + ejabberd.update_turn_configuration( + TurnConfiguration(None, new_turn_uris, new_shared_secret), + managed=False) + else: + ejabberd.update_turn_configuration(coturn.get_config(), + managed=True) + + @staticmethod + def _handle_MAM_configuration(old_config, new_config): + # note ejabberd action "enable" or "disable" restarts, if running + if new_config['MAM_enabled']: + actions.superuser_run('ejabberd', ['mam', 'enable']) + else: + actions.superuser_run('ejabberd', ['mam', 'disable']) + def form_valid(self, form): """Enable/disable a service and set messages.""" - old_status = form.initial - new_status = form.cleaned_data + old_config = form.initial + new_config = form.cleaned_data - if old_status['MAM_enabled'] != new_status['MAM_enabled']: - # note ejabberd action "enable" or "disable" restarts, if running - if new_status['MAM_enabled']: - actions.superuser_run('ejabberd', ['mam', 'enable']) - messages.success(self.request, - _('Message Archive Management enabled')) - else: - actions.superuser_run('ejabberd', ['mam', 'disable']) - messages.success(self.request, - _('Message Archive Management disabled')) + def changed(prop): + return old_config[prop] != new_config[prop] + + is_changed = False + if changed('MAM_enabled'): + self._handle_MAM_configuration(old_config, new_config) + is_changed = True + + if changed('enable_managed_turn') or changed('turn_uris') or \ + changed('shared_secret'): + self._handle_turn_configuration(old_config, new_config) + is_changed = True + + if is_changed: + messages.success(self.request, _('Configuration updated')) return super().form_valid(form)