From e0bfd1401fa6115535a8ea865e795000d2dc5ffb Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Wed, 11 Nov 2015 07:56:59 -0500 Subject: [PATCH] Name Services module --- data/etc/plinth/modules-enabled/names | 1 + plinth/modules/config/__init__.py | 4 +- plinth/modules/config/config.py | 35 +++++++ plinth/modules/names/__init__.py | 121 ++++++++++++++++++++++ plinth/modules/names/templates/names.html | 63 +++++++++++ plinth/modules/names/tests/__init__.py | 0 plinth/modules/names/tests/test_names.py | 96 +++++++++++++++++ plinth/modules/names/urls.py | 28 +++++ plinth/modules/names/views.py | 52 ++++++++++ plinth/modules/pagekite/__init__.py | 28 ++++- plinth/modules/pagekite/forms.py | 36 +++++++ plinth/modules/tor/__init__.py | 2 +- plinth/modules/tor/tor.py | 70 ++++++++++--- plinth/signals.py | 3 + 14 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 data/etc/plinth/modules-enabled/names create mode 100644 plinth/modules/names/__init__.py create mode 100644 plinth/modules/names/templates/names.html create mode 100644 plinth/modules/names/tests/__init__.py create mode 100644 plinth/modules/names/tests/test_names.py create mode 100644 plinth/modules/names/urls.py create mode 100644 plinth/modules/names/views.py 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 %} + + {% endfor %} + + + + {% for name_service in status.name_services %} + + + {% for service in name_service.services_enabled %} + + {% endfor %} + + {% endfor %} + +
+
{{ service }}
+
+ {{ name_service.type }}
+ {{ name_service.name }} +
+ {% if service %} + Enabled + {% else %} + Disabled + {% endif %} +
+
+
+ +{% 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'])