Name Services module

This commit is contained in:
James Valleroy 2015-11-11 07:56:59 -05:00 committed by fonfon
parent 5dfa2d0626
commit e0bfd1401f
14 changed files with 524 additions and 15 deletions

View File

@ -0,0 +1 @@
plinth.modules.names

View File

@ -24,4 +24,6 @@ from .config import init
__all__ = ['config', 'init'] __all__ = ['config', 'init']
depends = ['plinth.modules.system'] depends = ['plinth.modules.system',
'plinth.modules.firewall',
'plinth.modules.names']

View File

@ -33,8 +33,11 @@ import socket
from plinth import actions from plinth import actions
from plinth import cfg 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 pre_hostname_change, post_hostname_change
from plinth.signals import domainname_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])?$' 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', menu.add_urlname(ugettext_lazy('Configure'), 'glyphicon-cog',
'config:index', 10) '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): def index(request):
"""Serve the configuration form""" """Serve the configuration form"""
@ -228,3 +248,18 @@ def set_domainname(domainname):
domainname_change.send_robust(sender='config', domainname_change.send_robust(sender='config',
old_domainname=old_domainname, old_domainname=old_domainname,
new_domainname=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)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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]

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block content %}
<h2>Name Services</h2>
<div class="row">
<div class="col-sm-5">
<table class="table table-bordered table-condensed table-striped">
<thead>
<tr>
<td></td>
{% for service in status.services %}
<td>
<div class="text-center">{{ service }}</div>
</td>
{% endfor %}
</tr>
</thead>
<tbody>
{% for name_service in status.name_services %}
<tr>
<td>
<b>{{ name_service.type }}</b></br>
<i>{{ name_service.name }}</i>
</td>
{% for service in name_service.services_enabled %}
<td>
{% if service %}
<span class="label label-success">Enabled</span>
{% else %}
<span class="label label-warning">Disabled<span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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])

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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'),
)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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,
}

View File

@ -21,10 +21,13 @@ Plinth module to configure PageKite
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from plinth import cfg from plinth import cfg
from plinth.signals import domain_added
from . import utils
__all__ = ['init'] __all__ = ['init']
depends = ['plinth.modules.apps'] depends = ['plinth.modules.apps', 'plinth.modules.names']
def init(): def init():
@ -32,3 +35,26 @@ def init():
menu = cfg.main_menu.get('apps:index') menu = cfg.main_menu.get('apps:index')
menu.add_urlname(_('Public Visibility (PageKite)'), menu.add_urlname(_('Public Visibility (PageKite)'),
'glyphicon-flag', 'pagekite:index', 800) '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)

View File

@ -24,6 +24,7 @@ import json
import logging import logging
from plinth.errors import ActionError from plinth.errors import ActionError
from plinth.signals import domain_added, domain_removed
from . import utils from . import utils
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -103,6 +104,20 @@ class ConfigurationForm(forms.Form):
elif config_changed and new['enabled']: elif config_changed and new['enabled']:
utils.run(['restart']) 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): class StandardServiceForm(forms.Form):
"""Creates a form out of PREDEFINED_SERVICES""" """Creates a form out of PREDEFINED_SERVICES"""
@ -135,6 +150,27 @@ class StandardServiceForm(forms.Form):
messages.success(request, _('Service disabled: {name}') messages.success(request, _('Service disabled: {name}')
.format(name=service_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): class BaseCustomServiceForm(forms.Form):
"""Basic form functionality to handle a custom service""" """Basic form functionality to handle a custom service"""

View File

@ -28,7 +28,7 @@ from plinth import action_utils
__all__ = ['tor', 'init'] __all__ = ['tor', 'init']
depends = ['plinth.modules.apps'] depends = ['plinth.modules.apps', 'plinth.modules.names']
def diagnose(): def diagnose():

View File

@ -32,6 +32,8 @@ from plinth import action_utils
from plinth import cfg from plinth import cfg
from plinth import package from plinth import package
from plinth.errors import ActionError 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', APT_SOURCES_URI_PATHS = ('/files/etc/apt/sources.list/*/uri',
'/files/etc/apt/sources.list.d/*/*/uri') '/files/etc/apt/sources.list.d/*/*/uri')
@ -64,6 +66,25 @@ def init():
menu.add_urlname(_('Anonymity Network (Tor)'), 'glyphicon-eye-close', menu.add_urlname(_('Anonymity Network (Tor)'), 'glyphicon-eye-close',
'tor:index', 100) '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(): def on_install():
"""Setup Tor configuration as soon as it is installed.""" """Setup Tor configuration as soon as it is installed."""
@ -167,18 +188,7 @@ def is_apt_transport_tor_enabled():
return True return True
def get_status(): def get_hs():
"""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
output = actions.superuser_run('tor', ['get-hs']) output = actions.superuser_run('tor', ['get-hs'])
output = output.strip() output = output.strip()
if output == '': if output == '':
@ -195,6 +205,23 @@ def get_status():
hs_hostname = hs_info[0] hs_hostname = hs_info[0]
hs_ports = hs_info[1] 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'), return {'enabled': action_utils.service_is_enabled('tor'),
'is_running': action_utils.service_is_running('tor'), 'is_running': action_utils.service_is_running('tor'),
'ports': ports, 'ports': ports,
@ -238,6 +265,25 @@ def __apply_changes(request, old_status, new_status):
actions.superuser_run('tor', ['disable-hs']) actions.superuser_run('tor', ['disable-hs'])
messages.success(request, _('Tor hidden service disabled')) 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'] != \ if old_status['apt_transport_tor_enabled'] != \
new_status['apt_transport_tor_enabled']: new_status['apt_transport_tor_enabled']:
if new_status['apt_transport_tor_enabled']: if new_status['apt_transport_tor_enabled']:

View File

@ -28,3 +28,6 @@ post_module_loading = Signal()
pre_hostname_change = Signal(providing_args=['old_hostname', 'new_hostname']) pre_hostname_change = Signal(providing_args=['old_hostname', 'new_hostname'])
post_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']) 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'])