mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-07-29 12:09:37 +00:00
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:
parent
8c69858d43
commit
9009cdafd6
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 +
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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__')
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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']))
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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__')
|
||||
|
||||
@ -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'] = \
|
||||
|
||||
@ -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 #
|
||||
##############################
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user