config, names: Move domain name configuration to names app

Tests:

- Config app description is as expected.
- Config form does not show domain name field anymore.
  - Submitting the form with changes works.
- Names app has correct link for configuring static domain name. Clicking it
  takes to page for setting domain name.
- On startup, static domian name signal is sent properly if set. Otherwise no
  signal is send.
- Change domain name form shows correct value for current domain name.
- Change domain name form sets the value for domain name properly.
  - Page title is correct.
  - Validations works.
  - Add/remove domain name signals are sent properly.
  - Success message as shown expected
  - /etc/hosts is updated as expected.
- Unit tests work.
- Functional tests on ejabberd, letsencrypt, matrix, email, jsxc, openvpn
- After freshly starting the service. Visiting names app shows correct list of
  domains.
- ejabberd:
  - Installs works as expected. Currently set domain_name is setup properly.
    Copy certificate happens on proper domain.
  - Changing the domain sets the domain properly in ejabberd configuration.
  - Ejabberd app page shows link to name services instead of config app.
    Clicking works as expected.
- letsencrypt:
  - When no domains are configured, the link to 'Configure domains' is to the
    names app.
- matrix-synapse:
  - Domain name is properly shown in the status.
- email:
  - Primary domain name is shows properly in the app page.
  - Setting new primary domain works.
  - When installing, domain set as static domain name is prioritized as primary
    domain.
- jsxc:
  - Show the current static domain name in the domain field. BOSH server is
    available.
- openvpn:
  - Show the current static domain in profile is set otherwise show the current
    hostname.
  - If domain name is not set, downloaded OpenVPN profile shows hostname.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
Sunil Mohan Adapa 2024-09-11 14:49:21 -07:00 committed by Veiko Aasa
parent 8c69858d43
commit 9009cdafd6
No known key found for this signature in database
GPG Key ID: 478539CAE680674E
26 changed files with 229 additions and 240 deletions

View File

@ -1,8 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app for basic system configuration."""
import socket
import augeas
from django.utils.translation import gettext_lazy as _
@ -11,16 +9,14 @@ from plinth import frontpage, menu
from plinth.daemon import RelatedDaemon
from plinth.modules.apache import (get_users_with_website, user_of_uws_url,
uws_url_of_user)
from plinth.modules.names.components import DomainType
from plinth.package import Packages
from plinth.privileged import service as service_privileged
from plinth.signals import domain_added
from . import privileged
_description = [
_('Here you can set some general configuration options '
'like domain name, webserver home page etc.')
'like webserver home page etc.')
]
ADVANCED_MODE_KEY = 'advanced_mode'
@ -60,20 +56,6 @@ class ConfigApp(app_module.App):
daemon2 = RelatedDaemon('related-daemon-config2', 'rsyslog')
self.add(daemon2)
domain_type = DomainType('domain-type-static', _('Domain Name'),
'config:index', can_have_certificate=True)
self.add(domain_type)
@staticmethod
def post_init():
"""Perform post initialization operations."""
# Register domain with Name Services module.
domainname = get_domainname()
if domainname:
domain_added.send_robust(sender='config',
domain_type='domain-type-static',
name=domainname, services='__all__')
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
@ -95,12 +77,6 @@ class ConfigApp(app_module.App):
service_privileged.mask('rsyslog')
def get_domainname():
"""Return the domainname."""
fqdn = socket.getfqdn()
return '.'.join(fqdn.split('.')[1:])
def home_page_url2scid(url):
"""Return the shortcut ID of the given home page url."""
# url is None when the freedombox-apache-homepage configuration file does

View File

@ -3,12 +3,7 @@
Forms for basic system configuration
"""
import logging
import re
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
@ -18,17 +13,6 @@ from plinth.utils import format_lazy
from . import home_page_url2scid
logger = logging.getLogger(__name__)
HOSTNAME_REGEX = r'^[a-zA-Z0-9]([-a-zA-Z0-9]{,61}[a-zA-Z0-9])?$'
def domain_label_validator(domainname):
"""Validate domain name labels."""
for label in domainname.split('.'):
if not re.match(HOSTNAME_REGEX, label):
raise ValidationError(_('Invalid domain name'))
def get_homepage_choices():
"""Return list of drop down choices for home page."""
@ -47,23 +31,6 @@ def get_homepage_choices():
class ConfigurationForm(forms.Form):
"""Main system configuration form"""
domainname = forms.CharField(
label=gettext_lazy('Domain Name'), help_text=format_lazy(
gettext_lazy(
'Domain name is the global name by which other devices on the '
'Internet can reach your {box_name}. It must consist of '
'labels separated by dots. Each label must start and end '
'with an alphabet or a digit and have as interior characters '
'only alphabets, digits and hyphens. Length of each label '
'must be 63 characters or less. Total length of domain name '
'must be 253 characters or less.'),
box_name=gettext_lazy(cfg.box_name)), required=False, validators=[
validators.RegexValidator(
r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$',
gettext_lazy('Invalid domain name')),
domain_label_validator
], strip=True)
homepage = forms.ChoiceField(
label=gettext_lazy('Webserver Home Page'), help_text=format_lazy(
gettext_lazy(

View File

@ -3,7 +3,6 @@
import os
import pathlib
import subprocess
import augeas
@ -18,32 +17,6 @@ APACHE_HOMEPAGE_CONFIG = os.path.join(APACHE_CONF_ENABLED_DIR,
JOURNALD_FILE = pathlib.Path('/etc/systemd/journald.conf.d/50-freedombox.conf')
@privileged
def set_domainname(domainname: str | None = None):
"""Set system domainname in /etc/hosts."""
hostname = subprocess.check_output(['hostname']).decode().strip()
hosts_path = pathlib.Path('/etc/hosts')
if domainname:
insert_line = f'127.0.1.1 {hostname}.{domainname} {hostname}\n'
else:
insert_line = f'127.0.1.1 {hostname}\n'
lines = hosts_path.read_text(encoding='utf-8').splitlines(keepends=True)
new_lines = []
found = False
for line in lines:
if '127.0.1.1' in line:
new_lines.append(insert_line)
found = True
else:
new_lines.append(line)
if not found:
new_lines.append(insert_line)
hosts_path.write_text(''.join(new_lines), encoding='utf-8')
def load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +

View File

@ -12,37 +12,6 @@ from plinth import __main__ as plinth_main
from plinth.modules.apache import uws_directory_of_user, uws_url_of_user
from plinth.modules.config import (_home_page_scid2url, change_home_page,
get_home_page, home_page_url2scid)
from plinth.modules.config.forms import ConfigurationForm
def test_domainname_field():
"""Test that domainname field accepts only valid domainnames."""
valid_domainnames = [
'', 'a', '0a', 'a0', 'AAA', '00', '0-0', 'example-hostname', 'example',
'example.org', 'a.b.c.d', 'a-0.b-0.c-0',
'012345678901234567890123456789012345678901234567890123456789012',
((('x' * 63) + '.') * 3) + 'x' * 61
]
invalid_domainnames = [
'-', '-a', 'a-', '.a', 'a.', '?', 'a?a', 'a..a', 'a.-a', '.',
((('x' * 63) + '.') * 3) + 'x' * 62, 'x' * 64
]
for domainname in valid_domainnames:
form = ConfigurationForm({
'hostname': 'example',
'domainname': domainname,
'logging_mode': 'volatile'
})
assert form.is_valid()
for domainname in invalid_domainnames:
form = ConfigurationForm({
'hostname': 'example',
'domainname': domainname,
'logging_mode': 'volatile'
})
assert not form.is_valid()
def test_homepage_mapping():

View File

@ -7,10 +7,7 @@ import pytest
from plinth.tests import functional
pytestmark = [
pytest.mark.system, pytest.mark.essential, pytest.mark.domain,
pytest.mark.config
]
pytestmark = [pytest.mark.system, pytest.mark.essential, pytest.mark.config]
@pytest.fixture(scope='module', autouse=True)
@ -19,16 +16,6 @@ def fixture_background(session_browser):
functional.login(session_browser)
def test_change_domain_name(session_browser):
"""Test changing the domain name."""
functional.set_domain_name(session_browser, 'mydomain.example')
assert _get_domain_name(session_browser) == 'mydomain.example'
# Capitalization is ignored.
functional.set_domain_name(session_browser, 'Mydomain.example')
assert _get_domain_name(session_browser) == 'mydomain.example'
def test_change_home_page(session_browser):
"""Test changing webserver home page."""
functional.install(session_browser, 'syncthing')
@ -39,11 +26,6 @@ def test_change_home_page(session_browser):
assert _check_home_page_redirect(session_browser, 'plinth')
def _get_domain_name(browser):
functional.nav_to_module(browser, 'config')
return browser.find_by_id('id_domainname').value
def _set_home_page(browser, home_page):
if 'plinth' not in home_page and 'apache' not in home_page:
home_page = 'shortcut-' + home_page

View File

@ -1,20 +1,15 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox views for basic system configuration."""
import logging
from django.contrib import messages
from django.utils.translation import gettext as _
from plinth import views
from plinth.modules import config
from plinth.signals import domain_added, domain_removed
from . import privileged
from .forms import ConfigurationForm
LOGGER = logging.getLogger(__name__)
class ConfigAppView(views.AppView):
"""Serve configuration page."""
@ -25,7 +20,6 @@ class ConfigAppView(views.AppView):
def get_initial(self):
"""Return the current status."""
return {
'domainname': config.get_domainname(),
'homepage': config.get_home_page(),
'advanced_mode': config.get_advanced_mode(),
'logging_mode': privileged.get_logging_mode(),
@ -38,18 +32,6 @@ class ConfigAppView(views.AppView):
is_changed = False
if old_status['domainname'] != new_status['domainname']:
try:
set_domainname(new_status['domainname'],
old_status['domainname'])
except Exception as exception:
messages.error(
self.request,
_('Error setting domain name: {exception}').format(
exception=exception))
else:
messages.success(self.request, _('Domain name set'))
if old_status['homepage'] != new_status['homepage']:
try:
config.change_home_page(new_status['homepage'])
@ -85,26 +67,3 @@ class ConfigAppView(views.AppView):
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)
def set_domainname(domainname, old_domainname):
"""Set machine domain name to domainname."""
old_domainname = config.get_domainname()
# Domain name is not case sensitive, but Let's Encrypt certificate
# paths use lower-case domain name.
domainname = domainname.lower()
LOGGER.info('Changing domain name to - %s', domainname)
privileged.set_domainname(domainname)
# Update domain registered with Name Services module.
if old_domainname:
domain_removed.send_robust(sender='config',
domain_type='domain-type-static',
name=old_domainname)
if domainname:
domain_added.send_robust(sender='config',
domain_type='domain-type-static',
name=domainname, services='__all__')

View File

@ -11,7 +11,7 @@ from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon
from plinth.modules import config
from plinth.modules import names
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.coturn.components import TurnConfiguration, TurnConsumer
@ -136,15 +136,15 @@ class EjabberdApp(app_module.App):
def setup(self, old_version):
"""Install and configure the app."""
domainname = config.get_domainname()
logger.info('ejabberd service domainname - %s', domainname)
domain_name = names.get_domain_name()
logger.info('ejabberd service domain name - %s', domain_name)
privileged.pre_install(domainname)
privileged.pre_install(domain_name)
# XXX: Configure all other domain names
super().setup(old_version)
self.get_component('letsencrypt-ejabberd').setup_certificates(
[domainname])
privileged.setup(domainname)
[domain_name])
privileged.setup(domain_name)
if not old_version:
self.enable()

View File

@ -34,18 +34,18 @@ TURN_URI_REGEX = r'(stun|turn):(.*):([0-9]{4})(?:\?transport=(tcp|udp))?'
@privileged
def pre_install(domainname: str):
def pre_install(domain_name: str):
"""Preseed debconf values before packages are installed."""
if not domainname:
# If new domainname is blank, use hostname instead.
domainname = socket.gethostname()
if not domain_name:
# If new domain_name is blank, use hostname instead.
domain_name = socket.gethostname()
action_utils.debconf_set_selections(
['ejabberd ejabberd/hostname string ' + domainname])
['ejabberd ejabberd/hostname string ' + domain_name])
@privileged
def setup(domainname: str):
def setup(domain_name: str):
"""Enable LDAP authentication."""
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
conf = yaml.load(file_handle)
@ -86,7 +86,7 @@ def setup(domainname: str):
with open(EJABBERD_CONFIG, 'w', encoding='utf-8') as file_handle:
yaml.dump(conf, file_handle)
_upgrade_config(domainname)
_upgrade_config(domain_name)
try:
subprocess.check_output(['ejabberdctl', 'restart'])
@ -195,8 +195,8 @@ def get_domains() -> list[str]:
@privileged
def add_domain(domainname: str):
"""Update ejabberd with new domainname.
def add_domain(domain_name: str):
"""Update ejabberd with new domain name.
Restarting ejabberd is handled by letsencrypt-ejabberd component.
"""
@ -204,11 +204,11 @@ def add_domain(domainname: str):
logger.info('ejabberdctl not found')
return
# Add updated domainname to ejabberd hosts list.
# Add updated domain name to ejabberd hosts list.
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
conf = yaml.load(file_handle)
conf['hosts'].append(scalarstring.DoubleQuotedScalarString(domainname))
conf['hosts'].append(scalarstring.DoubleQuotedScalarString(domain_name))
conf['hosts'] = list(set(conf['hosts']))

View File

@ -13,17 +13,19 @@
<h3>{% trans "Status" %}</h3>
<p>
{% url 'config:index' as index_url %}
{% if domainname %}
{% blocktrans trimmed with domainname=domainname %}
Your XMPP server domain is set to <b>{{ domainname }}</b>. User
IDs will look like <i>username@{{ domainname }}</i>. You
{% url 'names:index' as names_url %}
{% if domain_name %}
{% blocktrans trimmed with domain_name=domain_name %}
Your XMPP server domain is set to <b>{{ domain_name }}</b>. User
IDs will look like <i>username@{{ domain_name }}</i>. You
can setup your domain on the system
<a href="{{ index_url }}">Configure</a> page.
<a href="{{ names_url }}">Name Services</a> page.
{% endblocktrans %}
{% else %}
Your XMPP server domain is not set. You can setup your domain on
the system <a href="{{ index_url }}">Configure</a> page.
{% blocktrans trimmed %}
Your XMPP server domain is not set. You can setup your domain on the
system <a href="{{ names_url }}">Name Services</a> page.
{% endblocktrans %}
{% endif %}
</p>

View File

@ -34,7 +34,7 @@ class EjabberdAppView(AppView):
"""Add service to the context data."""
context = super().get_context_data(*args, **kwargs)
domains = ejabberd.get_domains()
context['domainname'] = domains[0] if domains else None
context['domain_name'] = domains[0] if domains else None
return context
@staticmethod

View File

@ -12,7 +12,6 @@ from plinth.config import DropinConfigs
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.config import get_domainname
from plinth.modules.firewall.components import (Firewall,
FirewallLocalProtection)
from plinth.modules.letsencrypt.components import LetsEncrypt
@ -223,12 +222,6 @@ class EmailApp(plinth.app.App):
self.enable()
def get_domains():
"""Return the list of domains configured."""
default_domain = get_domainname()
return [default_domain] if default_domain else []
def _get_first_admin():
"""Return an admin user in the system or None if non exist."""
from django.contrib.auth.models import User

View File

@ -12,7 +12,7 @@ import re
from plinth.actions import privileged
from plinth.app import App
from plinth.modules import config
from plinth.modules import names
from plinth.modules.email import postfix
from plinth.modules.names.components import DomainName
@ -34,7 +34,7 @@ def set_all_domains(primary_domain=None):
if not primary_domain:
primary_domain = get_domains()['primary_domain']
if primary_domain not in all_domains:
primary_domain = config.get_domainname() or list(all_domains)[0]
primary_domain = names.get_domain_name() or list(all_domains)[0]
# Update configuration and don't restart daemons
set_domains(primary_domain, list(all_domains))

View File

@ -57,7 +57,7 @@
<script src="{% static 'jsxc/jsxc-plinth.js' %}"></script>
</head>
<body data-domain="{{ domainname }}"
<body data-domain="{{ domain_name }}"
data-jsxc-root="{% static 'jsxc/libjs-jsxc' %}">
<div class="container" id="content" role="main">
<div class="row">

View File

@ -5,7 +5,7 @@ from django.http import Http404
from django.views.generic import TemplateView
import plinth.app as app_module
from plinth.modules import config
from plinth.modules import names
class JsxcView(TemplateView):
@ -24,5 +24,5 @@ class JsxcView(TemplateView):
def get_context_data(self, *args, **kwargs):
"""Add domain information to view context."""
context = super().get_context_data(*args, **kwargs)
context['domainname'] = config.get_domainname()
context['domain_name'] = names.get_domain_name()
return context

View File

@ -108,9 +108,9 @@
</table>
</div>
{% else %}
{% url 'config:index' as config_url %}
{% url 'names:index' as names_url %}
{% blocktrans trimmed %}
No domains have been configured. <a href="{{ config_url }}">Configure
No domains have been configured. <a href="{{ names_url }}">Configure
domains</a> to be able to obtain certificates for them.
{% endblocktrans %}
{% endif %}

View File

@ -30,7 +30,7 @@
</div>
{% if not domain_names %}
{% url 'config:index' as config_url %}
{% url 'names:index' as config_url %}
<p>
{% blocktrans trimmed %}
No domain(s) are available. <a href="{{ config_url }}">Configure</a>

View File

@ -16,6 +16,7 @@ from plinth.daemon import Daemon
from plinth.diagnostic_check import (DiagnosticCheck,
DiagnosticCheckParameters, Result)
from plinth.modules.backups.components import BackupRestore
from plinth.modules.names.components import DomainType
from plinth.package import Packages
from plinth.privileged import service as service_privileged
from plinth.signals import (domain_added, domain_removed, post_hostname_change,
@ -64,6 +65,10 @@ class NamesApp(app_module.App):
['systemd-resolved', 'libnss-resolve', 'iproute2'])
self.add(packages)
domain_type = DomainType('domain-type-static', _('Domain Name'),
'names:domains', can_have_certificate=True)
self.add(domain_type)
daemon = Daemon('daemon-names', 'systemd-resolved')
self.add(daemon)
@ -77,6 +82,13 @@ class NamesApp(app_module.App):
domain_added.connect(on_domain_added)
domain_removed.connect(on_domain_removed)
# Register domain with Name Services module.
domain_name = get_domain_name()
if domain_name:
domain_added.send_robust(sender='names',
domain_type='domain-type-static',
name=domain_name, services='__all__')
def diagnose(self) -> list[DiagnosticCheck]:
"""Run diagnostics and return the results."""
results = super().diagnose()
@ -160,6 +172,12 @@ def on_domain_removed(sender, domain_type, name='', **kwargs):
######################################################
def get_domain_name():
"""Return the currently set static domain name."""
fqdn = socket.getfqdn()
return '.'.join(fqdn.split('.')[1:])
def get_hostname():
"""Return the hostname."""
return socket.gethostname()
@ -167,11 +185,8 @@ def get_hostname():
def set_hostname(hostname):
"""Set machine hostname and send signals before and after."""
from plinth.modules import config
from plinth.modules.config import privileged as config_privileged
old_hostname = get_hostname()
domainname = config.get_domainname()
domain_name = get_domain_name()
# Hostname should be ASCII. If it's unicode but passed our
# valid_hostname check, convert
@ -183,8 +198,8 @@ def set_hostname(hostname):
logger.info('Changing hostname to - %s', hostname)
privileged.set_hostname(hostname)
logger.info('Setting domain name after hostname change - %s', domainname)
config_privileged.set_domainname(domainname)
logger.info('Setting domain name after hostname change - %s', domain_name)
privileged.set_domain_name(domain_name)
post_hostname_change.send_robust(sender='names', old_hostname=old_hostname,
new_hostname=hostname)

View File

@ -1,8 +1,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Forms for the names app."""
import re
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from plinth import cfg
@ -87,3 +90,29 @@ class HostnameForm(forms.Form):
validators.RegexValidator(HOSTNAME_REGEX,
_('Invalid hostname'))
], strip=True)
def _domain_label_validator(domain_name):
"""Validate domain name labels."""
for label in domain_name.split('.'):
if not re.match(HOSTNAME_REGEX, label):
raise ValidationError(_('Invalid domain name'))
class DomainNameForm(forms.Form):
"""Form to update system's static domain name."""
domain_name = forms.CharField(
label=_('Domain Name'), help_text=format_lazy(
_('Domain name is the global name by which other devices on the '
'Internet can reach your {box_name}. It must consist of '
'labels separated by dots. Each label must start and end '
'with an alphabet or a digit and have as interior characters '
'only alphabets, digits and hyphens. Length of each label '
'must be 63 characters or less. Total length of domain name '
'must be 253 characters or less.'), box_name=_(cfg.box_name)),
required=False, validators=[
validators.RegexValidator(
r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$',
_('Invalid domain name')), _domain_label_validator
], strip=True)

View File

@ -26,6 +26,32 @@ def set_hostname(hostname: str):
action_utils.service_restart('avahi-daemon')
@privileged
def set_domain_name(domain_name: str | None = None):
"""Set system's static domain name in /etc/hosts."""
hostname = subprocess.check_output(['hostname']).decode().strip()
hosts_path = pathlib.Path('/etc/hosts')
if domain_name:
insert_line = f'127.0.1.1 {hostname}.{domain_name} {hostname}\n'
else:
insert_line = f'127.0.1.1 {hostname}\n'
lines = hosts_path.read_text(encoding='utf-8').splitlines(keepends=True)
new_lines = []
found = False
for line in lines:
if '127.0.1.1' in line:
new_lines.append(insert_line)
found = True
else:
new_lines.append(line)
if not found:
new_lines.append(insert_line)
hosts_path.write_text(''.join(new_lines), encoding='utf-8')
@privileged
def set_resolved_configuration(dns_fallback: bool | None = None,
dns_over_tls: str | None = None,

View File

@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tests for forms in names app."""
from ..forms import HostnameForm
from ..forms import DomainNameForm, HostnameForm
def test_hostname_field():
@ -22,3 +22,25 @@ def test_hostname_field():
for hostname in invalid_hostnames:
form = HostnameForm({'hostname': hostname})
assert not form.is_valid()
def test_domain_name_field():
"""Test that domain name field accepts only valid domain names."""
valid_domain_names = [
'', 'a', '0a', 'a0', 'AAA', '00', '0-0', 'example-hostname', 'example',
'example.org', 'a.b.c.d', 'a-0.b-0.c-0',
'012345678901234567890123456789012345678901234567890123456789012',
((('x' * 63) + '.') * 3) + 'x' * 61
]
invalid_domain_names = [
'-', '-a', 'a-', '.a', 'a.', '?', 'a?a', 'a..a', 'a.-a', '.',
((('x' * 63) + '.') * 3) + 'x' * 62, 'x' * 64
]
for domain_name in valid_domain_names:
form = DomainNameForm({'domain_name': domain_name})
assert form.is_valid()
for domain_name in invalid_domain_names:
form = DomainNameForm({'domain_name': domain_name})
assert not form.is_valid()

View File

@ -26,3 +26,18 @@ def test_change_hostname(session_browser):
def _get_hostname(browser):
functional.visit(browser, '/plinth/sys/names/hostname/')
return browser.find_by_id('id_hostname-hostname').value
def test_change_domain_name(session_browser):
"""Test changing the domain name."""
functional.set_domain_name(session_browser, 'mydomain.example')
assert _get_domain_name(session_browser) == 'mydomain.example'
# Capitalization is ignored.
functional.set_domain_name(session_browser, 'Mydomain.example')
assert _get_domain_name(session_browser) == 'mydomain.example'
def _get_domain_name(browser):
functional.visit(browser, '/plinth/sys/names/domains/')
return browser.find_by_id('id_domain-name-domain_name').value

View File

@ -40,4 +40,4 @@ def test_on_domain_removed():
# try to remove things that don't exist
on_domain_removed('', '')
with pytest.raises(KeyError):
on_domain_removed('', 'domainname', 'iiiii')
on_domain_removed('', 'some-domain-type', 'x-unknown-name')

View File

@ -11,4 +11,6 @@ urlpatterns = [
re_path(r'^sys/names/$', views.NamesAppView.as_view(), name='index'),
re_path(r'^sys/names/hostname/$', views.HostnameView.as_view(),
name='hostname'),
re_path(r'^sys/names/domains/$', views.DomainNameView.as_view(),
name='domains'),
]

View File

@ -3,16 +3,21 @@
FreedomBox app for name services.
"""
import logging
from django.contrib import messages
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic.edit import FormView
from plinth.modules import names
from plinth.signals import domain_added, domain_removed
from plinth.views import AppView
from . import components, privileged, resolved
from .forms import HostnameForm, NamesConfigurationForm
from .forms import DomainNameForm, HostnameForm, NamesConfigurationForm
logger = logging.getLogger(__name__)
class NamesAppView(AppView):
@ -93,6 +98,40 @@ class HostnameView(FormView):
return super().form_valid(form)
class DomainNameView(FormView):
"""View to update system's static domain name."""
template_name = 'form.html'
form_class = DomainNameForm
prefix = 'domain-name'
success_url = reverse_lazy('names:index')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Set Domain Name')
return context
def get_initial(self):
"""Return the values to fill in the form."""
initial = super().get_initial()
initial['domain_name'] = names.get_domain_name()
return initial
def form_valid(self, form):
"""Apply the form changes."""
if form.initial['domain_name'] != form.cleaned_data['domain_name']:
try:
set_domain_name(form.cleaned_data['domain_name'])
messages.success(self.request, _('Configuration updated'))
except Exception as exception:
messages.error(
self.request,
_('Error setting domain name: {exception}').format(
exception=exception))
return super().form_valid(form)
def get_status():
"""Get configured services per name."""
domains = components.DomainName.list()
@ -103,3 +142,26 @@ def get_status():
]
return {'domains': domains, 'unused_domain_types': unused_domain_types}
def set_domain_name(domain_name):
"""Set system's static domain name to domain_name."""
old_domain_name = names.get_domain_name()
# Domain name is not case sensitive, but Let's Encrypt certificate
# paths use lower-case domain name.
domain_name = domain_name.lower()
logger.info('Changing domain name to - %s', domain_name)
privileged.set_domain_name(domain_name)
# Update domain registered with Name Services module.
if old_domain_name:
domain_removed.send_robust(sender='names',
domain_type='domain-type-static',
name=old_domain_name)
if domain_name:
domain_added.send_robust(sender='names',
domain_type='domain-type-static',
name=domain_name, services='__all__')

View File

@ -5,7 +5,7 @@ import logging
from django.http import HttpResponse
from plinth.modules import config, names
from plinth.modules import names
from plinth.views import AppView
from . import privileged
@ -23,12 +23,12 @@ class OpenVPNAppView(AppView):
def profile(request):
"""Provide the user's profile for download."""
username = request.user.username
domainname = config.get_domainname()
domain_name = names.get_domain_name()
if not config.get_domainname():
domainname = names.get_hostname()
if not domain_name:
domain_name = names.get_hostname()
profile_string = privileged.get_profile(username, domainname)
profile_string = privileged.get_profile(username, domain_name)
response = HttpResponse(profile_string,
content_type='application/x-openvpn-profile')
response['Content-Disposition'] = \

View File

@ -461,15 +461,6 @@ def app_can_be_disabled(browser, app_name):
return bool(button)
#########################
# Domain name utilities #
#########################
def set_domain_name(browser, domain_name):
nav_to_module(browser, 'config')
browser.find_by_id('id_domainname').fill(domain_name)
submit(browser, form_class='form-configuration')
########################
# Front page utilities #
########################
@ -521,6 +512,12 @@ def set_hostname(browser, hostname):
submit(browser, form_class='form-hostname')
def set_domain_name(browser, domain_name):
visit(browser, '/plinth/sys/names/domains/')
browser.find_by_id('id_domain-name-domain_name').fill(domain_name)
submit(browser, form_class='form-domain-name')
##############################
# System -> Config utilities #
##############################