diff --git a/data/etc/plinth/modules-enabled/names b/data/etc/plinth/modules-enabled/names
new file mode 100644
index 000000000..39f4d4ed6
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/names
@@ -0,0 +1 @@
+plinth.modules.names
diff --git a/plinth/modules/config/__init__.py b/plinth/modules/config/__init__.py
index 5b72f8424..12cb341ff 100644
--- a/plinth/modules/config/__init__.py
+++ b/plinth/modules/config/__init__.py
@@ -24,4 +24,6 @@ from .config import init
__all__ = ['config', 'init']
-depends = ['plinth.modules.system']
+depends = ['plinth.modules.system',
+ 'plinth.modules.firewall',
+ 'plinth.modules.names']
diff --git a/plinth/modules/config/config.py b/plinth/modules/config/config.py
index 7404e8fd9..8fbe984da 100644
--- a/plinth/modules/config/config.py
+++ b/plinth/modules/config/config.py
@@ -33,8 +33,11 @@ import socket
from plinth import actions
from plinth import cfg
+from plinth.modules.firewall import firewall
+from plinth.modules.names import SERVICES
from plinth.signals import pre_hostname_change, post_hostname_change
from plinth.signals import domainname_change
+from plinth.signals import domain_added, domain_removed
HOSTNAME_REGEX = r'^[a-zA-Z0-9]([-a-zA-Z0-9]{,61}[a-zA-Z0-9])?$'
@@ -132,6 +135,23 @@ def init():
menu.add_urlname(ugettext_lazy('Configure'), 'glyphicon-cog',
'config:index', 10)
+ # Register domain with Name Services module.
+ domainname = get_domainname()
+ if domainname:
+ try:
+ domainname_services = firewall.get_enabled_services(
+ zone='external')
+ except actions.ActionError:
+ # This happens when firewalld is not installed.
+ # TODO: Are these services actually enabled?
+ domainname_services = [service[0] for service in SERVICES]
+ else:
+ domainname_services = None
+
+ domain_added.send_robust(sender='config', domain_type='domainname',
+ name=domainname, description=_('Domain Name'),
+ services=domainname_services)
+
def index(request):
"""Serve the configuration form"""
@@ -228,3 +248,18 @@ def set_domainname(domainname):
domainname_change.send_robust(sender='config',
old_domainname=old_domainname,
new_domainname=domainname)
+
+ # Update domain registered with Name Services module.
+ domain_removed.send_robust(sender='config', domain_type='domainname')
+ if domainname:
+ try:
+ domainname_services = firewall.get_enabled_services(
+ zone='external')
+ except actions.ActionError:
+ # This happens when firewalld is not installed.
+ # TODO: Are these services actually enabled?
+ domainname_services = [service[0] for service in SERVICES]
+
+ domain_added.send_robust(sender='config', domain_type='domainname',
+ name=domainname, description=_('Domain Name'),
+ services=domainname_services)
diff --git a/plinth/modules/names/__init__.py b/plinth/modules/names/__init__.py
new file mode 100644
index 000000000..671967572
--- /dev/null
+++ b/plinth/modules/names/__init__.py
@@ -0,0 +1,121 @@
+#
+# This file is part of Plinth.
+#
+# 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 .
+#
+
+"""
+Plinth module to configure name services
+"""
+
+from gettext import gettext as _
+import logging
+
+from plinth import cfg
+from plinth.signals import domain_added, domain_removed
+
+
+SERVICES = [
+ ('http', _('HTTP'), 80),
+ ('https', _('HTTPS'), 443),
+ ('ssh', _('SSH'), 22),
+]
+
+depends = ['plinth.modules.system']
+
+domain_types = {}
+domains = {}
+
+logger = logging.getLogger(__name__)
+
+
+def init():
+ """Initialize the names module."""
+ menu = cfg.main_menu.get('system:index')
+ menu.add_urlname(_('Name Services'), 'glyphicon-th', 'names:index', 19)
+
+ domain_added.connect(on_domain_added)
+ domain_removed.connect(on_domain_removed)
+
+
+def on_domain_added(sender, domain_type, name='', description='',
+ services=None, **kwargs):
+ """Add domain to global list."""
+ if not domain_type:
+ return
+ domain_types[domain_type] = description
+
+ if not name:
+ return
+ if not services:
+ services = []
+
+ if domain_type not in domains:
+ # new domain_type
+ domains[domain_type] = {}
+ domains[domain_type][name] = services
+ logger.info('Added domain %s of type %s with services %s',
+ name, domain_type, str(services))
+
+
+def on_domain_removed(sender, domain_type, name='', **kwargs):
+ """Remove domain from global list."""
+ if domain_type in domains:
+ if name == '': # remove all domains of this type
+ domains[domain_type] = {}
+ logger.info('Removed all domains of type %s', domain_type)
+ elif name in domains[domain_type]:
+ del domains[domain_type][name]
+ logger.info('Removed domain %s of type %s', name, domain_type)
+
+
+def get_domain_types():
+ """Get list of domain_types."""
+ return list(domain_types.keys())
+
+
+def get_description(domain_type):
+ """Get description of a domain_type, if available."""
+ if domain_type in domain_types:
+ return domain_types[domain_type]
+ else:
+ return domain_type
+
+
+def get_domain(domain_type):
+ """
+ Get domain of type domain_type.
+
+ This function is meant for use with single-domain domain_types. If there is
+ more than one domain, any one of the domains may be returned.
+ """
+ if domain_type in domains and len(domains[domain_type]) > 0:
+ return list(domains[domain_type].keys())[0]
+ else:
+ return _('Not Available')
+
+
+def get_services(domain_type, domain):
+ """Get list of enabled services for a domain."""
+ try:
+ return domains[domain_type][domain]
+ except KeyError:
+ # domain_type or domain not registered
+ return []
+
+
+def get_services_status(domain_type, domain):
+ """Get list of whether each service is enabled for a domain."""
+ enabled = get_services(domain_type, domain)
+ return [service[0] in enabled for service in SERVICES]
diff --git a/plinth/modules/names/templates/names.html b/plinth/modules/names/templates/names.html
new file mode 100644
index 000000000..bfd198b85
--- /dev/null
+++ b/plinth/modules/names/templates/names.html
@@ -0,0 +1,63 @@
+{% extends "base.html" %}
+{% comment %}
+#
+# This file is part of Plinth.
+#
+# 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 .
+#
+{% endcomment %}
+
+{% load bootstrap %}
+
+{% block content %}
+
+
Name Services
+
+
+
+
+
+
+ |
+ {% for service in status.services %}
+
+ {{ service }}
+ |
+ {% endfor %}
+
+
+
+ {% for name_service in status.name_services %}
+
+ |
+ {{ name_service.type }}
+ {{ name_service.name }}
+ |
+ {% for service in name_service.services_enabled %}
+
+ {% if service %}
+ Enabled
+ {% else %}
+ Disabled
+ {% endif %}
+ |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/plinth/modules/names/tests/__init__.py b/plinth/modules/names/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/modules/names/tests/test_names.py b/plinth/modules/names/tests/test_names.py
new file mode 100644
index 000000000..6c98ca9d4
--- /dev/null
+++ b/plinth/modules/names/tests/test_names.py
@@ -0,0 +1,96 @@
+#
+# This file is part of Plinth.
+#
+# 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 .
+#
+
+"""
+Tests for names module.
+"""
+
+import unittest
+
+from .. import domain_types, domains
+from .. import on_domain_added, on_domain_removed
+from .. import get_domain_types, get_description
+from .. import get_domain, get_services, get_services_status
+
+
+class TestNames(unittest.TestCase):
+ """Test cases for testing the names module."""
+ def test_on_domain_added(self):
+ """Test adding a domain to the global list."""
+ on_domain_added('', '')
+ self.assertNotIn('', domain_types)
+ self.assertNotIn('', domains)
+
+ on_domain_added('', 'hiddenservice', 'ddddd.onion')
+ on_domain_added('', 'hiddenservice', 'eeeee.onion')
+ self.assertIn('ddddd.onion', domains['hiddenservice'])
+ self.assertIn('eeeee.onion', domains['hiddenservice'])
+
+ def test_on_domain_removed(self):
+ """Test removing a domain from the global list."""
+ on_domain_added('', 'domainname', 'fffff')
+ on_domain_removed('', 'domainname', 'fffff')
+ self.assertNotIn('fffff', domains['domainname'])
+
+ on_domain_added('', 'pagekite', 'ggggg.pagekite.me')
+ on_domain_added('', 'pagekite', 'hhhhh.pagekite.me')
+ on_domain_removed('', 'pagekite')
+ self.assertNotIn('ggggg.pagekite.me', domains['pagekite'])
+ self.assertNotIn('hhhhh.pagekite.me', domains['pagekite'])
+
+ # try to remove things that don't exist
+ on_domain_removed('', '')
+ on_domain_removed('', 'domainname', 'iiiii')
+
+ def test_get_domain_types(self):
+ """Test getting domain types."""
+ on_domain_added('', 'domainname')
+ self.assertIn('domainname', get_domain_types())
+
+ def test_get_description(self):
+ """Test getting domain type description."""
+ on_domain_added('', 'pagekite', '', 'Pagekite')
+ self.assertEqual(get_description('pagekite'), 'Pagekite')
+
+ self.assertEqual('asdfasdf', get_description('asdfasdf'))
+
+ def test_get_domain(self):
+ """Test getting a domain of domain_type."""
+ on_domain_added('', 'hiddenservice', 'aaaaa.onion')
+ self.assertEqual(get_domain('hiddenservice'), 'aaaaa.onion')
+
+ self.assertEqual('Not Available', get_domain('abcdef'))
+
+ on_domain_removed('', 'hiddenservice')
+ self.assertEqual('Not Available', get_domain('hiddenservice'))
+
+ def test_get_services(self):
+ """Test getting enabled services for a domain."""
+ on_domain_added('', 'domainname', 'bbbbb', '',
+ ['http', 'https', 'ssh'])
+ self.assertEqual(get_services('domainname', 'bbbbb'),
+ ['http', 'https', 'ssh'])
+
+ self.assertEqual(get_services('xxxxx', 'yyyyy'), [])
+ self.assertEqual(get_services('domainname', 'zzzzz'), [])
+
+ def test_get_services_status(self):
+ """Test getting whether each service is enabled for a domain."""
+ on_domain_added('', 'pagekite', 'ccccc.pagekite.me', '',
+ ['http', 'https'])
+ self.assertEqual(get_services_status('pagekite', 'ccccc.pagekite.me'),
+ [True, True, False])
diff --git a/plinth/modules/names/urls.py b/plinth/modules/names/urls.py
new file mode 100644
index 000000000..ee8cec746
--- /dev/null
+++ b/plinth/modules/names/urls.py
@@ -0,0 +1,28 @@
+#
+# This file is part of Plinth.
+#
+# 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 .
+#
+
+"""
+URLs for the name services module
+"""
+
+from django.conf.urls import patterns, url
+
+
+urlpatterns = patterns(
+ 'plinth.modules.names.views',
+ url(r'^sys/names/$', 'index', name='index'),
+)
diff --git a/plinth/modules/names/views.py b/plinth/modules/names/views.py
new file mode 100644
index 000000000..28bcaf611
--- /dev/null
+++ b/plinth/modules/names/views.py
@@ -0,0 +1,52 @@
+#
+# This file is part of Plinth.
+#
+# 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 .
+#
+
+"""
+Plinth module for name services
+"""
+
+from django.template.response import TemplateResponse
+from gettext import gettext as _
+
+from . import SERVICES, get_domain_types, get_description
+from . import get_domain, get_services_status
+
+
+def index(request):
+ """Serve name services page."""
+ status = get_status()
+
+ return TemplateResponse(request, 'names.html',
+ {'title': _('Name Services'),
+ 'status': status})
+
+
+def get_status():
+ """Get configured services per name."""
+ name_services = []
+ for domain_type in sorted(get_domain_types()):
+ domain = get_domain(domain_type)
+ name_services.append({
+ 'type': get_description(domain_type),
+ 'name': domain,
+ 'services_enabled': get_services_status(domain_type, domain),
+ })
+
+ return {
+ 'services': [service[1] for service in SERVICES],
+ 'name_services': name_services,
+ }
diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py
index 40015d1f5..00a99e37f 100644
--- a/plinth/modules/pagekite/__init__.py
+++ b/plinth/modules/pagekite/__init__.py
@@ -21,10 +21,13 @@ Plinth module to configure PageKite
from django.utils.translation import ugettext_lazy as _
from plinth import cfg
+from plinth.signals import domain_added
+
+from . import utils
__all__ = ['init']
-depends = ['plinth.modules.apps']
+depends = ['plinth.modules.apps', 'plinth.modules.names']
def init():
@@ -32,3 +35,26 @@ def init():
menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Public Visibility (PageKite)'),
'glyphicon-flag', 'pagekite:index', 800)
+
+ # Register kite name with Name Services module.
+ try:
+ kite_name = utils.get_kite_details()['kite_name']
+ enabled = utils.get_pagekite_config()['enabled']
+ except IndexError:
+ # no data from 'pagekite get-kite'
+ kite_name = None
+ enabled_services = None
+ else:
+ if enabled and kite_name:
+ services = utils.get_pagekite_services()[0]
+ enabled_services = []
+ for service in services:
+ if services[service]:
+ enabled_services.append(service)
+ else:
+ kite_name = None
+ enabled_services = None
+
+ domain_added.send_robust(sender='pagekite', domain_type='pagekite',
+ name=kite_name, description=_('Pagekite'),
+ services=enabled_services)
diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py
index 314405af7..61a18cc4c 100644
--- a/plinth/modules/pagekite/forms.py
+++ b/plinth/modules/pagekite/forms.py
@@ -24,6 +24,7 @@ import json
import logging
from plinth.errors import ActionError
+from plinth.signals import domain_added, domain_removed
from . import utils
LOGGER = logging.getLogger(__name__)
@@ -103,6 +104,20 @@ class ConfigurationForm(forms.Form):
elif config_changed and new['enabled']:
utils.run(['restart'])
+ # Update kite name registered with Name Services module.
+ domain_removed.send_robust(
+ sender='pagekite', domain_type='pagekite')
+ if new['enabled'] and new['kite_name']:
+ services = utils.get_pagekite_services()[0]
+ enabled_services = []
+ for service in services:
+ if services[service]:
+ enabled_services.append(service)
+ domain_added.send_robust(
+ sender='pagekite', domain_type='pagekite',
+ name=new['kite_name'], description=_('Pagekite'),
+ services=enabled_services)
+
class StandardServiceForm(forms.Form):
"""Creates a form out of PREDEFINED_SERVICES"""
@@ -135,6 +150,27 @@ class StandardServiceForm(forms.Form):
messages.success(request, _('Service disabled: {name}')
.format(name=service_name))
+ # Update kite name services registered with Name Services module.
+ domain_removed.send_robust(
+ sender='pagekite', domain_type='pagekite')
+ try:
+ kite_name = utils.get_kite_details()['kite_name']
+ enabled = utils.get_pagekite_config()['enabled']
+ except IndexError:
+ # no data from 'pagekite get-kite'
+ pass
+ else:
+ if enabled and kite_name:
+ services = utils.get_pagekite_services()[0]
+ enabled_services = []
+ for service in services:
+ if services[service]:
+ enabled_services.append(service)
+ domain_added.send_robust(
+ sender='pagekite', domain_type='pagekite',
+ name=kite_name, description=_('Pagekite'),
+ services=enabled_services)
+
class BaseCustomServiceForm(forms.Form):
"""Basic form functionality to handle a custom service"""
diff --git a/plinth/modules/tor/__init__.py b/plinth/modules/tor/__init__.py
index a40ca4f19..514b9f78d 100644
--- a/plinth/modules/tor/__init__.py
+++ b/plinth/modules/tor/__init__.py
@@ -28,7 +28,7 @@ from plinth import action_utils
__all__ = ['tor', 'init']
-depends = ['plinth.modules.apps']
+depends = ['plinth.modules.apps', 'plinth.modules.names']
def diagnose():
diff --git a/plinth/modules/tor/tor.py b/plinth/modules/tor/tor.py
index b6ef8345a..c961327d7 100644
--- a/plinth/modules/tor/tor.py
+++ b/plinth/modules/tor/tor.py
@@ -32,6 +32,8 @@ from plinth import action_utils
from plinth import cfg
from plinth import package
from plinth.errors import ActionError
+from plinth.modules.names import SERVICES
+from plinth.signals import domain_added, domain_removed
APT_SOURCES_URI_PATHS = ('/files/etc/apt/sources.list/*/uri',
'/files/etc/apt/sources.list.d/*/*/uri')
@@ -64,6 +66,25 @@ def init():
menu.add_urlname(_('Anonymity Network (Tor)'), 'glyphicon-eye-close',
'tor:index', 100)
+ # Register hidden service name with Name Services module.
+ enabled = action_utils.service_is_enabled('tor')
+ is_running = action_utils.service_is_running('tor')
+ (hs_enabled, hs_hostname, hs_ports) = get_hs()
+
+ if enabled and is_running and hs_enabled and hs_hostname:
+ hs_services = []
+ for service in SERVICES:
+ if str(service[2]) in hs_ports:
+ hs_services.append(service[0])
+ else:
+ hs_hostname = None
+ hs_services = None
+
+ domain_added.send_robust(
+ sender='tor', domain_type='hiddenservice',
+ name=hs_hostname, description=_('Tor Hidden Service'),
+ services=hs_services)
+
def on_install():
"""Setup Tor configuration as soon as it is installed."""
@@ -167,18 +188,7 @@ def is_apt_transport_tor_enabled():
return True
-def get_status():
- """Return the current status"""
- output = actions.superuser_run('tor', ['get-ports'])
- port_info = output.split('\n')
- ports = {}
- for line in port_info:
- try:
- (key, val) = line.split()
- ports[key] = val
- except ValueError:
- continue
-
+def get_hs():
output = actions.superuser_run('tor', ['get-hs'])
output = output.strip()
if output == '':
@@ -195,6 +205,23 @@ def get_status():
hs_hostname = hs_info[0]
hs_ports = hs_info[1]
+ return (hs_enabled, hs_hostname, hs_ports)
+
+
+def get_status():
+ """Return the current status"""
+ output = actions.superuser_run('tor', ['get-ports'])
+ port_info = output.split('\n')
+ ports = {}
+ for line in port_info:
+ try:
+ (key, val) = line.split()
+ ports[key] = val
+ except ValueError:
+ continue
+
+ (hs_enabled, hs_hostname, hs_ports) = get_hs()
+
return {'enabled': action_utils.service_is_enabled('tor'),
'is_running': action_utils.service_is_running('tor'),
'ports': ports,
@@ -238,6 +265,25 @@ def __apply_changes(request, old_status, new_status):
actions.superuser_run('tor', ['disable-hs'])
messages.success(request, _('Tor hidden service disabled'))
+ # Update hidden service name registered with Name Services module.
+ domain_removed.send_robust(
+ sender='tor', domain_type='hiddenservice')
+
+ enabled = action_utils.service_is_enabled('tor')
+ is_running = action_utils.service_is_running('tor')
+ (hs_enabled, hs_hostname, hs_ports) = get_hs()
+
+ if enabled and is_running and hs_enabled and hs_hostname:
+ hs_services = []
+ for service in SERVICES:
+ if str(service[2]) in hs_ports:
+ hs_services.append(service[0])
+
+ domain_added.send_robust(
+ sender='tor', domain_type='hiddenservice',
+ name=hs_hostname, description=_('Tor Hidden Service'),
+ services=hs_services)
+
if old_status['apt_transport_tor_enabled'] != \
new_status['apt_transport_tor_enabled']:
if new_status['apt_transport_tor_enabled']:
diff --git a/plinth/signals.py b/plinth/signals.py
index 568c437fe..f6473f9e2 100644
--- a/plinth/signals.py
+++ b/plinth/signals.py
@@ -28,3 +28,6 @@ post_module_loading = Signal()
pre_hostname_change = Signal(providing_args=['old_hostname', 'new_hostname'])
post_hostname_change = Signal(providing_args=['old_hostname', 'new_hostname'])
domainname_change = Signal(providing_args=['old_domainname', 'new_domainname'])
+domain_added = Signal(providing_args=['domain_type', 'name', 'description',
+ 'services'])
+domain_removed = Signal(providing_args=['domain_type', 'name'])