mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-01 09:30:29 +00:00
ejabberd: STUN/TURN configuration
This implementation is very similar to that of Matrix Synapse with a lot of code duplicated. One major difference is that ejabberd doesn't have a conf.d/ directory. So, the managed configuration and overridden configuration cannot be cleanly separated. Whether the configuration is managed or not is determined by the presence of a file under `/etc/ejabberd`. Managed coturn configuration isn't stored in ejabberd, since only one set of configuration can be stored at a time. If the admin chooses to use the managed configuration, the current coturn configuration is fetched and used to configure ejabberd. Fixes #1978 Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
d70816d0e5
commit
7a30470cb5
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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 '
|
||||
'<a href="{users_url}"> user with a {box_name} login</a>.'),
|
||||
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 <a href={coturn_url}>Coturn</a> 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)
|
||||
|
||||
@ -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 <a href={coturn_url}>coturn</a> 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()])
|
||||
|
||||
35
plinth/modules/ejabberd/static/ejabberd.js
Normal file
35
plinth/modules/ejabberd/static/ejabberd.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @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();
|
||||
});
|
||||
@ -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 @@
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript"
|
||||
src="{% static 'ejabberd/ejabberd.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
78
plinth/modules/ejabberd/tests/conftest.py
Normal file
78
plinth/modules/ejabberd/tests/conftest.py
Normal file
@ -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
|
||||
231
plinth/modules/ejabberd/tests/data/ejabberd.yml.example
Normal file
231
plinth/modules/ejabberd/tests/data/ejabberd.yml.example
Normal file
@ -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
|
||||
48
plinth/modules/ejabberd/tests/test_turn_config.py
Normal file
48
plinth/modules/ejabberd/tests/test_turn_config.py
Normal file
@ -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)
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user