mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
monkeysphere: New module for verifying SSH servers
monkeysphere: Run publish as background task, allow user to cancel. Small fixes to names module: - Remove unused ugettext import. - Change SERVICES to tuple. - If a domain is not available for a service type, return None instead of (translated) "Not Available". - Rename get_services -> get_enabled_services.
This commit is contained in:
parent
447f067734
commit
70d85cbd6f
126
actions/monkeysphere
Executable file
126
actions/monkeysphere
Executable file
@ -0,0 +1,126 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Configuration helper for monkeysphere.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
HOST_TOOL = 'monkeysphere-host'
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Return parsed command line arguments as dictionary."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||
|
||||
host_show_key = subparsers.add_parser('host-show-key',
|
||||
help='Show host key fingerprint')
|
||||
host_show_key.add_argument('keyid', nargs='*',
|
||||
help='Optional list of KEYIDs')
|
||||
|
||||
host_import_ssh_key = subparsers.add_parser('host-import-ssh-key',
|
||||
help='Import host SSH key')
|
||||
host_import_ssh_key.add_argument('hostname',
|
||||
help='Fully-qualified hostname')
|
||||
|
||||
host_publish_key = subparsers.add_parser(
|
||||
'host-publish-key',
|
||||
help='Push host key to keyserver')
|
||||
host_publish_key.add_argument(
|
||||
'keyid', nargs='*',
|
||||
help='Optional list of KEYIDs')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def subcommand_host_show_key(arguments):
|
||||
"""Show host key fingerprint."""
|
||||
keyid = ' '.join(arguments.keyid)
|
||||
try:
|
||||
output = subprocess.check_output([HOST_TOOL, 'show-key', keyid])
|
||||
except subprocess.CalledProcessError:
|
||||
# no keys available
|
||||
return
|
||||
|
||||
# parse output
|
||||
keys = [dict()]
|
||||
lines = output.decode().strip().split('\n')
|
||||
for line in lines:
|
||||
if line.startswith('pub'):
|
||||
data = line.lstrip('pub').split()
|
||||
keys[-1]['pub'] = data[0]
|
||||
keys[-1]['date'] = data[1]
|
||||
elif line.startswith('uid'):
|
||||
keys[-1]['uid'] = line.lstrip('uid').strip()
|
||||
elif line.startswith('OpenPGP fingerprint:'):
|
||||
keys[-1]['pgp_fingerprint'] = line.lstrip('Open PGP fingerprint:')
|
||||
elif line.startswith('ssh fingerprint:'):
|
||||
data = line.lstrip('ssh fingerprint:').split()
|
||||
keys[-1]['ssh_keysize'] = data[0]
|
||||
keys[-1]['ssh_fingerprint'] = data[1]
|
||||
keys[-1]['ssh_keytype'] = data[2].strip('()')
|
||||
elif line == '':
|
||||
keys.append(dict())
|
||||
|
||||
for key in keys:
|
||||
print(key['pub'], key['date'], key['uid'], key['pgp_fingerprint'],
|
||||
key['ssh_keysize'], key['ssh_fingerprint'], key['ssh_keytype'])
|
||||
|
||||
|
||||
def subcommand_host_import_ssh_key(arguments):
|
||||
"""Import host SSH key."""
|
||||
output = subprocess.check_output(
|
||||
[HOST_TOOL, 'import-key',
|
||||
'/etc/ssh/ssh_host_rsa_key', arguments.hostname])
|
||||
print(output.decode())
|
||||
|
||||
|
||||
def subcommand_host_publish_key(arguments):
|
||||
"""Push host key to keyserver."""
|
||||
keyid = ' '.join(arguments.keyid)
|
||||
# setting TMPDIR as workaround for Debian bug #656750
|
||||
proc = subprocess.Popen(
|
||||
[HOST_TOOL, 'publish-keys', keyid],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
|
||||
env=dict(
|
||||
os.environ,
|
||||
TMPDIR='/var/lib/monkeysphere/authentication/tmp/',
|
||||
MONKEYSPHERE_PROMPT='false'))
|
||||
output, error = proc.communicate()
|
||||
output, error = output.decode(), error.decode()
|
||||
if proc.returncode != 0:
|
||||
raise Exception(output, error)
|
||||
|
||||
print(output)
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse arguments and perform all duties."""
|
||||
arguments = parse_arguments()
|
||||
|
||||
subcommand = arguments.subcommand.replace('-', '_')
|
||||
subcommand_method = globals()['subcommand_' + subcommand]
|
||||
subcommand_method(arguments)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
data/etc/plinth/modules-enabled/monkeysphere
Normal file
1
data/etc/plinth/modules-enabled/monkeysphere
Normal file
@ -0,0 +1 @@
|
||||
plinth.modules.monkeysphere
|
||||
33
plinth/modules/monkeysphere/__init__.py
Normal file
33
plinth/modules/monkeysphere/__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# 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 monkeysphere.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth import cfg
|
||||
|
||||
depends = ['plinth.modules.system']
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the monkeysphere module."""
|
||||
menu = cfg.main_menu.get('system:index')
|
||||
menu.add_urlname(_('Monkeysphere'), 'glyphicon-certificate',
|
||||
'monkeysphere:index', 970)
|
||||
108
plinth/modules/monkeysphere/templates/monkeysphere.html
Normal file
108
plinth/modules/monkeysphere/templates/monkeysphere.html
Normal file
@ -0,0 +1,108 @@
|
||||
{% 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 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_head %}
|
||||
|
||||
{% if running %}
|
||||
<meta http-equiv="refresh" content="3"/>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>{% trans "Monkeysphere" %}</h2>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed with box_name=cfg.box_name %}
|
||||
With Monkeysphere, a PGP key can be generated for each domain
|
||||
serving SSH on this {{ box_name }}. The PGP public key can then be
|
||||
uploaded to the PGP keyservers. Users connecting to this {{ box_name }}
|
||||
through SSH can verify that they are connecting to the correct
|
||||
host. See the
|
||||
<a href="http://web.monkeysphere.info/getting-started-ssh/">
|
||||
Monkeysphere SSH documentation</a> for more details.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% if running %}
|
||||
<p class="running-status-parent">
|
||||
<span class="running-status active"></span>
|
||||
{% trans "Publishing key to keyserver..." %}
|
||||
|
||||
<form class="form" method="post"
|
||||
action="{% url 'monkeysphere:cancel' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<button type="submit" class="btn btn-warning btn-sm">
|
||||
{% trans "Cancel" %}</button>
|
||||
</form>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
{% for name_service in status.name_services %}
|
||||
<tr>
|
||||
<td>
|
||||
<b>{{ name_service.type }}</b><br>
|
||||
<i>{{ name_service.name }}</i>
|
||||
</td>
|
||||
<td>
|
||||
{% if name_service.key %}
|
||||
{{ name_service.key.pgp_fingerprint }}
|
||||
{% else %}
|
||||
{% trans "Not Available" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if name_service.available %}
|
||||
{% if not name_service.key %}
|
||||
<form class="form" method="post"
|
||||
action="{% url 'monkeysphere:generate' name_service.short_type %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm pull-right">
|
||||
{% trans "Generate PGP Key" %}</button>
|
||||
</form>
|
||||
|
||||
{% elif not running %}
|
||||
<form class="form" method="post"
|
||||
action="{% url 'monkeysphere:publish' name_service.key.pgp_fingerprint %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<button type="submit" class="btn btn-warning btn-sm pull-right">
|
||||
{% trans "Publish Key" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
0
plinth/modules/monkeysphere/tests/__init__.py
Normal file
0
plinth/modules/monkeysphere/tests/__init__.py
Normal file
34
plinth/modules/monkeysphere/urls.py
Normal file
34
plinth/modules/monkeysphere/urls.py
Normal file
@ -0,0 +1,34 @@
|
||||
#
|
||||
# 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 monkeysphere module.
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sys/monkeysphere/$', views.index, name='index'),
|
||||
url(r'^sys/monkeysphere/(?P<service>[\w]+)/generate/$',
|
||||
views.generate, name='generate'),
|
||||
url(r'^sys/monkeysphere/(?P<fingerprint>[\w]+)/publish/$',
|
||||
views.publish, name='publish'),
|
||||
url(r'^sys/monkeysphere/cancel/$', views.cancel, name='cancel'),
|
||||
]
|
||||
144
plinth/modules/monkeysphere/views.py
Normal file
144
plinth/modules/monkeysphere/views.py
Normal file
@ -0,0 +1,144 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Views for the monkeysphere module.
|
||||
"""
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from plinth import actions
|
||||
from plinth import package
|
||||
from plinth.modules import names
|
||||
|
||||
publish_process = None
|
||||
|
||||
|
||||
@package.required(['monkeysphere'])
|
||||
def index(request):
|
||||
"""Serve configuration page."""
|
||||
_collect_publish_result(request)
|
||||
status = get_status()
|
||||
return TemplateResponse(
|
||||
request, 'monkeysphere.html',
|
||||
{'title': _('Monkeysphere'),
|
||||
'status': status,
|
||||
'running': bool(publish_process)})
|
||||
|
||||
|
||||
@require_POST
|
||||
def generate(request, service):
|
||||
"""Generate PGP key for SSH service."""
|
||||
for domain_type in sorted(names.get_domain_types()):
|
||||
if domain_type == service:
|
||||
domain = names.get_domain(domain_type)
|
||||
|
||||
try:
|
||||
actions.superuser_run(
|
||||
'monkeysphere',
|
||||
['host-import-ssh-key', 'ssh://' + domain])
|
||||
messages.success(request, _('Generated PGP key'))
|
||||
except actions.ActionError as exception:
|
||||
messages.error(request, str(exception))
|
||||
|
||||
return redirect(reverse_lazy('monkeysphere:index'))
|
||||
|
||||
|
||||
@require_POST
|
||||
def publish(request, fingerprint):
|
||||
"""Publish PGP key for SSH service."""
|
||||
global publish_process
|
||||
if not publish_process:
|
||||
publish_process = actions.superuser_run(
|
||||
'monkeysphere', ['host-publish-key', fingerprint], async=True)
|
||||
|
||||
return redirect(reverse_lazy('monkeysphere:index'))
|
||||
|
||||
|
||||
@require_POST
|
||||
def cancel(request):
|
||||
"""Cancel ongoing process."""
|
||||
global publish_process
|
||||
if publish_process:
|
||||
publish_process.terminate()
|
||||
publish_process = None
|
||||
messages.info(request, _('Cancelled publish key.'))
|
||||
|
||||
return redirect(reverse_lazy('monkeysphere:index'))
|
||||
|
||||
|
||||
def get_status():
|
||||
"""Get the current status."""
|
||||
output = actions.superuser_run('monkeysphere', ['host-show-key'])
|
||||
keys = []
|
||||
for line in output.split('\n'):
|
||||
data = line.strip().split()
|
||||
if data and len(data) == 7:
|
||||
keys.append(dict())
|
||||
keys[-1]['pub'] = data[0]
|
||||
keys[-1]['date'] = data[1]
|
||||
keys[-1]['uid'] = data[2]
|
||||
keys[-1]['name'] = data[2].replace('ssh://', '')
|
||||
keys[-1]['pgp_fingerprint'] = data[3]
|
||||
keys[-1]['ssh_keysize'] = data[4]
|
||||
keys[-1]['ssh_fingerprint'] = data[5]
|
||||
keys[-1]['ssh_keytype'] = data[6]
|
||||
|
||||
name_services = []
|
||||
for domain_type in sorted(names.get_domain_types()):
|
||||
domain = names.get_domain(domain_type)
|
||||
name_services.append({
|
||||
'type': names.get_description(domain_type),
|
||||
'short_type': domain_type,
|
||||
'name': domain or _('Not Available'),
|
||||
'available': bool(domain),
|
||||
'key': None,
|
||||
})
|
||||
|
||||
# match up keys with name services
|
||||
for key in keys:
|
||||
for name_service in name_services:
|
||||
if key['name'] == name_service['name']:
|
||||
name_service['key'] = key
|
||||
continue
|
||||
|
||||
return {'name_services': name_services}
|
||||
|
||||
|
||||
def _collect_publish_result(request):
|
||||
"""Handle publish process completion."""
|
||||
global publish_process
|
||||
if not publish_process:
|
||||
return
|
||||
|
||||
return_code = publish_process.poll()
|
||||
|
||||
# Publish process is not complete yet
|
||||
if return_code is None:
|
||||
return
|
||||
|
||||
if not return_code:
|
||||
messages.success(request, _('Published key to keyserver.'))
|
||||
else:
|
||||
messages.error(request, _('Error occurred while publishing key.'))
|
||||
|
||||
publish_process = None
|
||||
@ -19,18 +19,17 @@
|
||||
Plinth module to configure name services
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import logging
|
||||
|
||||
from plinth import cfg
|
||||
from plinth.signals import domain_added, domain_removed
|
||||
|
||||
|
||||
SERVICES = [
|
||||
('http', ugettext_lazy('HTTP'), 80),
|
||||
('https', ugettext_lazy('HTTPS'), 443),
|
||||
('ssh', ugettext_lazy('SSH'), 22),
|
||||
]
|
||||
SERVICES = (
|
||||
('http', _('HTTP'), 80),
|
||||
('https', _('HTTPS'), 443),
|
||||
('ssh', _('SSH'), 22),
|
||||
)
|
||||
|
||||
depends = ['plinth.modules.system']
|
||||
|
||||
@ -43,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
def init():
|
||||
"""Initialize the names module."""
|
||||
menu = cfg.main_menu.get('system:index')
|
||||
menu.add_urlname(ugettext_lazy('Name Services'), 'glyphicon-tag',
|
||||
menu.add_urlname(_('Name Services'), 'glyphicon-tag',
|
||||
'names:index', 19)
|
||||
|
||||
domain_added.connect(on_domain_added)
|
||||
@ -104,10 +103,10 @@ def get_domain(domain_type):
|
||||
if domain_type in domains and len(domains[domain_type]) > 0:
|
||||
return list(domains[domain_type].keys())[0]
|
||||
else:
|
||||
return _('Not Available')
|
||||
return None
|
||||
|
||||
|
||||
def get_services(domain_type, domain):
|
||||
def get_enabled_services(domain_type, domain):
|
||||
"""Get list of enabled services for a domain."""
|
||||
try:
|
||||
return domains[domain_type][domain]
|
||||
@ -118,5 +117,5 @@ def get_services(domain_type, domain):
|
||||
|
||||
def get_services_status(domain_type, domain):
|
||||
"""Get list of whether each service is enabled for a domain."""
|
||||
enabled = get_services(domain_type, domain)
|
||||
enabled = get_enabled_services(domain_type, domain)
|
||||
return [service[0] in enabled for service in SERVICES]
|
||||
|
||||
@ -24,7 +24,7 @@ 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
|
||||
from .. import get_domain, get_enabled_services, get_services_status
|
||||
|
||||
|
||||
class TestNames(unittest.TestCase):
|
||||
@ -73,20 +73,20 @@ class TestNames(unittest.TestCase):
|
||||
on_domain_added('', 'hiddenservice', 'aaaaa.onion')
|
||||
self.assertEqual(get_domain('hiddenservice'), 'aaaaa.onion')
|
||||
|
||||
self.assertEqual('Not Available', get_domain('abcdef'))
|
||||
self.assertEqual(None, get_domain('abcdef'))
|
||||
|
||||
on_domain_removed('', 'hiddenservice')
|
||||
self.assertEqual('Not Available', get_domain('hiddenservice'))
|
||||
self.assertEqual(None, get_domain('hiddenservice'))
|
||||
|
||||
def test_get_services(self):
|
||||
def test_get_enabled_services(self):
|
||||
"""Test getting enabled services for a domain."""
|
||||
on_domain_added('', 'domainname', 'bbbbb', '',
|
||||
['http', 'https', 'ssh'])
|
||||
self.assertEqual(get_services('domainname', 'bbbbb'),
|
||||
self.assertEqual(get_enabled_services('domainname', 'bbbbb'),
|
||||
['http', 'https', 'ssh'])
|
||||
|
||||
self.assertEqual(get_services('xxxxx', 'yyyyy'), [])
|
||||
self.assertEqual(get_services('domainname', 'zzzzz'), [])
|
||||
self.assertEqual(get_enabled_services('xxxxx', 'yyyyy'), [])
|
||||
self.assertEqual(get_enabled_services('domainname', 'zzzzz'), [])
|
||||
|
||||
def test_get_services_status(self):
|
||||
"""Test getting whether each service is enabled for a domain."""
|
||||
|
||||
@ -42,7 +42,7 @@ def get_status():
|
||||
domain = get_domain(domain_type)
|
||||
name_services.append({
|
||||
'type': get_description(domain_type),
|
||||
'name': domain,
|
||||
'name': domain or _('Not Available'),
|
||||
'services_enabled': get_services_status(domain_type, domain),
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user