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']
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 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)

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 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)

View File

@ -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"""

View File

@ -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():

View File

@ -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']:

View File

@ -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'])