mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-03-11 09:04:54 +00:00
coturn: Add new component for usage of coturn by other apps
Sunil: - Add TurnConfiguration to reference documentation. Add more details in docstrings. - Rename the component to TurnConsumer since 'Component' in the name is redundant and unconventional. Also, hopefully, the component will retain the API over multiple TURN servers. - Log when notifying other components about configuration change. - Use TurnConfiguration class more widely. - Refactor for simplicity. - Additional tests. - Move URI generation code into TurnConfiguration. Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net> Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
db993ecb16
commit
ceacde67b9
10
doc/dev/reference/components/coturn.rst
Normal file
10
doc/dev/reference/components/coturn.rst
Normal file
@ -0,0 +1,10 @@
|
||||
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
|
||||
Coturn
|
||||
^^^^^^
|
||||
|
||||
.. autoclass:: plinth.modules.coturn.components.TurnConsumer
|
||||
:members:
|
||||
|
||||
.. autoclass:: plinth.modules.coturn.components.TurnConfiguration
|
||||
:members:
|
||||
@ -16,6 +16,7 @@ Components
|
||||
letsencrypt
|
||||
staticfiles
|
||||
backups
|
||||
coturn
|
||||
|
||||
Base Classes
|
||||
^^^^^^^^^^^^
|
||||
|
||||
@ -4,6 +4,7 @@ FreedomBox app to configure Coturn server.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -14,6 +15,7 @@ from plinth import menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules import names
|
||||
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
|
||||
@ -39,6 +41,8 @@ _description = [
|
||||
|
||||
app = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CoturnApp(app_module.App):
|
||||
"""FreedomBox app for Coturn."""
|
||||
@ -98,6 +102,7 @@ def setup(helper, old_version=None):
|
||||
helper.call('post', actions.superuser_run, 'coturn', ['setup'])
|
||||
helper.call('post', app.enable)
|
||||
app.get_component('letsencrypt-coturn').setup_certificates()
|
||||
notify_configuration_change()
|
||||
|
||||
|
||||
def get_available_domains():
|
||||
@ -109,8 +114,8 @@ def get_available_domains():
|
||||
def get_domain():
|
||||
"""Read TLS domain from config file select first available if none."""
|
||||
config = get_config()
|
||||
if config['realm']:
|
||||
return get_config()['realm']
|
||||
if config.domain:
|
||||
return config.domain
|
||||
|
||||
domain = next(get_available_domains(), None)
|
||||
set_domain(domain)
|
||||
@ -135,9 +140,19 @@ def set_domain(domain):
|
||||
"""Set the TLS domain by writing a file to data directory."""
|
||||
if domain:
|
||||
actions.superuser_run('coturn', ['set-domain', domain])
|
||||
notify_configuration_change()
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Return the coturn server configuration."""
|
||||
output = actions.superuser_run('coturn', ['get-config'])
|
||||
return json.loads(output)
|
||||
config = json.loads(output)
|
||||
return TurnConfiguration(config['realm'], [], config['static_auth_secret'])
|
||||
|
||||
|
||||
def notify_configuration_change():
|
||||
"""Notify all coturn components about the new configuration."""
|
||||
logger.info('Notifying STUN/TURN consumers about configuration change')
|
||||
config = get_config()
|
||||
for component in TurnConsumer.list():
|
||||
component.on_config_change(config)
|
||||
|
||||
90
plinth/modules/coturn/components.py
Normal file
90
plinth/modules/coturn/components.py
Normal file
@ -0,0 +1,90 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""App component for other apps to manage their STUN/TURN server configuration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from plinth import app
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnConfiguration:
|
||||
"""Data class to hold TURN server configuration.
|
||||
|
||||
domain is the string representing the domain name with which Coturn has
|
||||
been configured. This is necessary to associate the correct TLS certificate
|
||||
with Coturn communication. STUN/TURN URIs are generated using this.
|
||||
|
||||
shared_secret is a string that must be used by a server to be accepted by
|
||||
Coturn server. This is the value set in Coturn configuration file.
|
||||
|
||||
uris are a list of strings that represent the full set of STUN/TURN URIs
|
||||
that must be used by a STUN/TURN client after advice from the server.
|
||||
|
||||
"""
|
||||
domain: str = None
|
||||
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']
|
||||
]
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Return a JSON representation of the configuration."""
|
||||
return json.dumps({
|
||||
'domain': self.domain,
|
||||
'uris': self.uris,
|
||||
'shared_secret': self.shared_secret
|
||||
})
|
||||
|
||||
|
||||
class TurnConsumer(app.FollowerComponent):
|
||||
"""Component to manage coturn configuration.
|
||||
|
||||
In order to provide audio/video calling functionality, communication
|
||||
servers very often use an external server such as Coturn for implementing
|
||||
the STUN/TURN protocol. To use Coturn, the server needs to be configured
|
||||
with a set of URIs provided by Coturn along with a shared secret.
|
||||
|
||||
This component when added to an app allows the app to retrieve the current
|
||||
Coturn configuration and respond to any future configuration changes.
|
||||
|
||||
"""
|
||||
|
||||
_all = {}
|
||||
|
||||
def __init__(self, component_id):
|
||||
"""Initialize the component.
|
||||
|
||||
component_id should be a unique ID across all components of an app and
|
||||
across all components.
|
||||
|
||||
"""
|
||||
super().__init__(component_id)
|
||||
self._all[component_id] = self
|
||||
|
||||
@classmethod
|
||||
def list(cls) -> List[TurnConsumer]:
|
||||
"""Return a list of all Coturn components."""
|
||||
return cls._all.values()
|
||||
|
||||
def on_config_change(self, config: TurnConfiguration):
|
||||
"""Add or update STUN/TURN configuration.
|
||||
|
||||
Override this method and change app's configuration.
|
||||
|
||||
"""
|
||||
|
||||
def get_configuration(self) -> TurnConfiguration:
|
||||
"""Return current coturn configuration."""
|
||||
from plinth.modules import coturn
|
||||
return coturn.get_config()
|
||||
75
plinth/modules/coturn/tests/test_components.py
Normal file
75
plinth/modules/coturn/tests/test_components.py
Normal file
@ -0,0 +1,75 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Tests for the Coturn app component.
|
||||
"""
|
||||
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.utils import random_string
|
||||
|
||||
from .. import notify_configuration_change
|
||||
from ..components import TurnConfiguration, TurnConsumer
|
||||
|
||||
|
||||
@pytest.fixture(name='turn_configuration')
|
||||
def fixture_turn_configuration():
|
||||
"""Return test Coturn configuration."""
|
||||
return TurnConfiguration('test-domain.example', [], random_string(64))
|
||||
|
||||
|
||||
@pytest.fixture(name='empty_component_list', autouse=True)
|
||||
def fixture_empty_component_list():
|
||||
"""Remove all entries in component list before starting a test."""
|
||||
TurnConsumer._all = {}
|
||||
|
||||
|
||||
def test_configuration_init():
|
||||
"""Test creating configuration object."""
|
||||
config = TurnConfiguration('test-domain.example', [], 'test-shared-secret')
|
||||
assert config.domain == 'test-domain.example'
|
||||
assert config.shared_secret == 'test-shared-secret'
|
||||
assert config.uris == [
|
||||
"stun:test-domain.example:3478?transport=tcp",
|
||||
"stun:test-domain.example:3478?transport=udp",
|
||||
"turn:test-domain.example:3478?transport=tcp",
|
||||
"turn:test-domain.example:3478?transport=udp",
|
||||
]
|
||||
|
||||
config = TurnConfiguration(None, ['test-uri1', 'test-uri2'],
|
||||
'test-shared-secret')
|
||||
assert config.domain is None
|
||||
assert config.uris == ['test-uri1', 'test-uri2']
|
||||
|
||||
config = TurnConfiguration('test-domain.example',
|
||||
['test-uri1', 'test-uri2'],
|
||||
'test-shared-secret')
|
||||
assert config.domain == 'test-domain.example'
|
||||
assert config.uris == ['test-uri1', 'test-uri2']
|
||||
|
||||
|
||||
def test_component_init_and_list():
|
||||
"""Test initializing and listing all the components."""
|
||||
component1 = TurnConsumer('component1')
|
||||
component2 = TurnConsumer('component2')
|
||||
assert component1.component_id == 'component1'
|
||||
assert [component1, component2] == list(TurnConsumer.list())
|
||||
|
||||
|
||||
@patch('plinth.modules.coturn.get_config')
|
||||
def test_notify_on_configuration_changed(get_config, turn_configuration):
|
||||
"""Test configuration change notifications."""
|
||||
component = TurnConsumer('component')
|
||||
get_config.return_value = turn_configuration
|
||||
with patch.object(component, 'on_config_change') as mock_method:
|
||||
notify_configuration_change()
|
||||
mock_method.assert_has_calls([call(turn_configuration)])
|
||||
|
||||
|
||||
@patch('plinth.modules.coturn.get_config')
|
||||
def test_get_configuration(get_config, turn_configuration):
|
||||
"""Test coturn configuration retrieval using component."""
|
||||
get_config.return_value = turn_configuration
|
||||
component = TurnConsumer('component')
|
||||
assert component.get_configuration() == turn_configuration
|
||||
Loading…
x
Reference in New Issue
Block a user