From 1fc0064fd0e3217bfe929dbde66d2536b0b22cd2 Mon Sep 17 00:00:00 2001 From: fonfon Date: Wed, 14 Jan 2015 19:14:27 +0000 Subject: [PATCH] refactoring pagekite: configuration form works splitting the services to a separate page is not yet finished --- actions/pagekite | 23 +-- actions/pagekite_common.py | 52 ------ actions/pagekite_util.py | 104 +++++++++++ plinth/modules/pagekite/__init__.py | 10 +- plinth/modules/pagekite/forms.py | 169 ++++++++++++++++++ plinth/modules/pagekite/pagekite.py | 37 +--- .../templates/pagekite_configure.html | 13 +- .../templates/pagekite_custom_services.html | 95 ++++++++++ .../templates/pagekite_default_services.html | 55 ++++++ .../templates/pagekite_introduction.html | 8 +- plinth/modules/pagekite/urls.py | 13 +- plinth/modules/pagekite/util.py | 141 +++++++++++++++ plinth/modules/pagekite/views.py | 123 +++++++++++++ plinth/templatetags/plinth_extras.py | 19 ++ plinth/tests/test_pagekite_actions.py | 79 ++++++++ 15 files changed, 818 insertions(+), 123 deletions(-) delete mode 100644 actions/pagekite_common.py create mode 100644 actions/pagekite_util.py create mode 100644 plinth/modules/pagekite/forms.py create mode 100644 plinth/modules/pagekite/templates/pagekite_custom_services.html create mode 100644 plinth/modules/pagekite/templates/pagekite_default_services.html create mode 100644 plinth/modules/pagekite/util.py create mode 100644 plinth/modules/pagekite/views.py create mode 100644 plinth/tests/test_pagekite_actions.py diff --git a/actions/pagekite b/actions/pagekite index 561a31427..34bbaadaa 100755 --- a/actions/pagekite +++ b/actions/pagekite @@ -28,11 +28,11 @@ import augeas import os import subprocess -from pagekite_common import SERVICE_PARAMS, construct_params +from pagekite_util import SERVICE_PARAMS, construct_params, \ + deconstruct_params, get_augeas_servicefile_path, CONF_PATH aug = augeas.Augeas() -CONF_PATH = '/files/etc/pagekite.d' PATHS = { 'service_on': os.path.join(CONF_PATH, '*', 'service_on', '*'), 'kitename': os.path.join(CONF_PATH, '10_account.rc', 'kitename'), @@ -226,7 +226,7 @@ def subcommand_add_service(arguments): # so do it manually here path = convert_augeas_path_to_filepath(root) with open(path, 'a') as servicefile: - line = "service_on = %s" % arguments.params + line = "service_on = %s" % deconstruct_params(params) servicefile.write(line) @@ -251,23 +251,6 @@ def get_new_service_path(protocol): return os.path.join(root, str(new_index)) -def get_augeas_servicefile_path(protocol): - """Get the augeas path where a service for a protocol should be stored""" - if protocol == 'http': - relpath = '80_httpd.rc' - elif protocol == 'https': - relpath = '443_https.rc' - elif protocol == 'raw/22': - relpath = '22_ssh.rc' - elif protocol.startswith('raw'): - port = protocol.split('/')[1] - relpath = '%s.rc' % port - else: - raise ValueError('Unsupported protocol: %s' % protocol) - - return os.path.join(CONF_PATH, relpath, 'service_on') - - def subcommand_get_kite(_): """Print details of the currently configured kite""" kitename = aug.get(PATHS['kitename']) diff --git a/actions/pagekite_common.py b/actions/pagekite_common.py deleted file mode 100644 index df552041e..000000000 --- a/actions/pagekite_common.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/python2 -# -*- mode: python -*- -# -# 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 . -# - -""" -The variables/functions defined here are used by both the action script -and the plinth pagekite module. - -Currently that's functionality for converting pagekite service_on strings like - "http:@kitename:localhost:80:@kitestring" -into parameter dictionaries and the other way round. -""" - -SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', - 'secret'] - - -def construct_params(string): - """ Convert a parameter string into a params dictionary""" - try: - params = dict(zip(SERVICE_PARAMS, string.split(':'))) - except: - msg = """params are expected to be a ':'-separated string - containing values for: %s , for example:\n"--params - http:@kitename:localhost:8000:@kitesecret" - """ - raise ValueError(msg % ", ".join(SERVICE_PARAMS)) - return params - - -def deconstruct_params(params): - """ Convert params into a ":"-separated parameter string """ - try: - paramstring = ":".join([params[param] for param in SERVICE_PARAMS]) - except KeyError: - raise ValueError("Could not parse params: %s " % params) - return paramstring diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py new file mode 100644 index 000000000..9a258e7f9 --- /dev/null +++ b/actions/pagekite_util.py @@ -0,0 +1,104 @@ +#!/usr/bin/python2 +# -*- mode: python -*- +# +# 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 . +# + +""" +The variables/functions defined here are used by both the action script +and the plinth pagekite module. + +For example the functionality to convert pagekite service_on strings like + "http:@kitename:localhost:80:@kitestring" +into parameter dictionaries and the other way round. And functions that we want +to be covered by tests. +""" +# ATTENTION: This file has to be both python2 and python3 compatible + +import os +import shlex +CONF_PATH = '/files/etc/pagekite.d' + +SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port', + 'secret'] + + +def construct_params(string): + """ Convert a parameter string into a params dictionary""" + # The actions.py uses shlex.quote() to escape/quote malicious user input. + # That affects '*.@kitename', so the params string gets quoted. + # If the string is escaped and contains '*.@kitename', look whether shlex + # would still quote/escape the string when we remove '*.@kitename'. + + # TODO: use shlex only once augeas-python supports python3 + if hasattr(shlex, 'quote'): + quotefunction = shlex.quote + else: + import pipes + quotefunction = pipes.quote + + if string.startswith("'") and string.endswith("'"): + unquoted_string = string[1:-1] + error_msg = "The parameters contain suspicious characters: %s " + if '*.@kitename' in string: + unquoted_test_string = unquoted_string.replace('*.@kitename', '') + if unquoted_test_string == quotefunction(unquoted_test_string): + # no other malicious characters found, use the unquoted string + string = unquoted_string + else: + raise RuntimeError(error_msg % string) + else: + raise RuntimeError(error_msg % string) + + try: + params = dict(zip(SERVICE_PARAMS, string.split(':'))) + except: + msg = """params are expected to be a ':'-separated string containing + values for: %s , for example:\n"--params + http/8000:@kitename:localhost:8000:@kitesecret" + """ + raise ValueError(msg % ", ".join(SERVICE_PARAMS)) + return params + + +def deconstruct_params(params): + """ Convert params into a ":"-separated parameter string """ + try: + paramstring = ":".join([str(params[param]) for param in + SERVICE_PARAMS]) + except KeyError: + raise ValueError("Could not parse params: %s " % params) + return paramstring + + +def get_augeas_servicefile_path(protocol): + """Get the augeas path where a service for a protocol should be stored""" + if not protocol.startswith(("http", "https", "raw")): + raise ValueError('Unsupported protocol: %s' % protocol) + + try: + _protocol, port = protocol.split('/') + except ValueError: + if protocol == 'http': + relpath = '80_http.rc' + elif protocol == 'https': + relpath = '443_https.rc' + else: + raise ValueError('Unsupported protocol: %s' % protocol) + else: + relpath = '%s_%s.rc' % (port, _protocol) + + return os.path.join(CONF_PATH, relpath, 'service_on') diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py index 0d836a04f..b9ad5fb14 100644 --- a/plinth/modules/pagekite/__init__.py +++ b/plinth/modules/pagekite/__init__.py @@ -19,9 +19,17 @@ Plinth module to configure PageKite """ +from gettext import gettext as _ +from plinth import cfg from . import pagekite -from .pagekite import init __all__ = ['pagekite', 'init'] depends = ['plinth.modules.apps'] + + +def init(): + """Intialize the PageKite module""" + menu = cfg.main_menu.get('apps:index') + menu.add_urlname(_('Public Visibility (PageKite)'), + 'glyphicon-flag', 'pagekite:index', 50) diff --git a/plinth/modules/pagekite/forms.py b/plinth/modules/pagekite/forms.py new file mode 100644 index 000000000..f19c59a92 --- /dev/null +++ b/plinth/modules/pagekite/forms.py @@ -0,0 +1,169 @@ +# +# 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 . +# + +from gettext import gettext as _ +import logging + +from django import forms +from django.contrib import messages +from django.core import validators + +from actions.pagekite_util import deconstruct_params +from .util import PREDEFINED_SERVICES, _run, get_kite_details, KITE_NAME, \ + KITE_SECRET, BACKEND_HOST + +LOGGER = logging.getLogger(__name__) + + +class TrimmedCharField(forms.CharField): + """Trim the contents of a CharField""" + def clean(self, value): + """Clean and validate the field value""" + if value: + value = value.strip() + + return super(TrimmedCharField, self).clean(value) + + +class ConfigurationForm(forms.Form): + """Configure PageKite credentials and frontend""" + + enabled = forms.BooleanField(label=_('Enable PageKite'), + required=False) + + server = forms.CharField( + label=_('Server'), required=False, + help_text=_('Select your pagekite.net server. Set "pagekite.net" to ' + 'use the default pagekite.net server'), + widget=forms.TextInput()) + + kite_name = TrimmedCharField( + label=_('Kite name'), + help_text=_('Example: mybox1-myacc.pagekite.me'), + validators=[ + validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$', + _('Invalid kite name'))]) + + kite_secret = TrimmedCharField( + label=_('Kite secret'), + help_text=_('A secret associated with the kite or the default secret \ +for your account if no secret is set on the kite')) + + def save(self, request): + old = self.initial + new = self.cleaned_data + LOGGER.info('New status is - %s', new) + + if old != new: + _run(['stop']) + + if old['enabled'] != new['enabled']: + if new['enabled']: + _run(['enable']) + messages.success(request, _('PageKite enabled')) + else: + _run(['disable']) + messages.success(request, _('PageKite disabled')) + + if old['kite_name'] != new['kite_name'] or \ + old['kite_secret'] != new['kite_secret']: + _run(['set-kite', '--kite-name', new['kite_name'], + '--kite-secret', new['kite_secret']]) + messages.success(request, _('Kite details set')) + + if old['server'] != new['server']: + server = new['server'] + if server in ('defaults', 'default', 'pagekite.net'): + _run(['enable-pagekitenet-frontend']) + else: + _run(['set-frontend', server]) + messages.success(request, _('Pagekite server set')) + + if old != new: + _run(['start']) + + +class DefaultServiceForm(forms.Form): + """Constructs a form out of PREDEFINED_SERVICES""" + + def __init__(self, *args, **kwargs): + """Add the fields from PREDEFINED_SERVICES""" + super(DefaultServiceForm, self).__init__(*args, **kwargs) + kite = get_kite_details() + for name, service in PREDEFINED_SERVICES.items(): + if name in ('http', 'https'): + help_text = service['help_text'].format(kite['kite_name']) + else: + help_text = service['help_text'] + self.fields[name] = forms.BooleanField(label=service['label'], + help_text=help_text, + required=False) + + def save(self, request): + formdata = self.cleaned_data + for service in PREDEFINED_SERVICES.keys(): + if self.initial[service] != formdata[service]: + params = PREDEFINED_SERVICES[service]['params'] + param_line = deconstruct_params(params) + if formdata[service]: + _run(['add-service', '--params', param_line]) + messages.success(request, _('Service enabled: {service}') + .format(service=service)) + else: + _run(['remove-service', '--params', param_line]) + messages.success(request, _('Service disabled: {service}') + .format(service=service)) + + +class CustomServiceForm(forms.Form): + """Form to add/delete a custom service""" + choices = [("http", "http"), ("https", "https"), ("raw", "raw")] + protocol = forms.ChoiceField(choices=choices, label="protocol") + frontend_port = forms.IntegerField(min_value=0, max_value=65535, + label="external (frontend) port") + backend_port = forms.IntegerField(min_value=0, max_value=65535, + label="internal (freedombox) port") + subdomains = forms.BooleanField(label="Enable Subdomains", required=False) + + def prepare_user_input_for_storage(self, params): + """prepare the user input for being stored via the action""" + # set kitename and kitesecret if not already set + if 'kitename' not in params: + if 'subdomains' in params and params['subdomains']: + params['kitename'] = "*.%s" % KITE_NAME + else: + params['kitename'] = KITE_NAME + if 'secret' not in params: + params['secret'] = KITE_SECRET + + # condense protocol and frontend_port to one entry (protocol) + if 'frontend_port' in params: + if str(params['frontend_port']) not in params['protocol']: + params['protocol'] = "%s/%s" % (params['protocol'], + params['frontend_port']) + if 'backend_host' not in params: + params['backend_host'] = BACKEND_HOST + + return deconstruct_params(params) + + def save(self, request): + params = self.prepare_user_input_for_storage(self.cleaned_data) + _run(['add-service', '--params', params]) + + def delete(self, request): + params = self.prepare_user_input_for_storage(self.cleaned_data) + _run(['remove-service', '--params', params]) diff --git a/plinth/modules/pagekite/pagekite.py b/plinth/modules/pagekite/pagekite.py index af2b34bc0..a22c961d9 100644 --- a/plinth/modules/pagekite/pagekite.py +++ b/plinth/modules/pagekite/pagekite.py @@ -19,49 +19,16 @@ Plinth module for configuring PageKite service """ -from django import forms -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.core import validators -from django.core.urlresolvers import reverse_lazy -from django.template.response import TemplateResponse -from gettext import gettext as _ -import logging - -from plinth import actions -from plinth import cfg -from plinth import package -LOGGER = logging.getLogger(__name__) - -subsubmenu = [{'url': reverse_lazy('pagekite:index'), - 'text': _('About PageKite')}, - {'url': reverse_lazy('pagekite:configure'), - 'text': _('Configure PageKite')}] -def init(): - """Intialize the PageKite module""" - menu = cfg.main_menu.get('apps:index') - menu.add_urlname(_('Public Visibility (PageKite)'), - 'glyphicon-flag', 'pagekite:index', 50) -@login_required -def index(request): - """Serve introduction page""" - return TemplateResponse(request, 'pagekite_introduction.html', - {'title': _('Public Visibility (PageKite)'), - 'subsubmenu': subsubmenu}) -class TrimmedCharField(forms.CharField): - """Trim the contents of a CharField""" - def clean(self, value): - """Clean and validate the field value""" - if value: - value = value.strip() + + return super(TrimmedCharField, self).clean(value) diff --git a/plinth/modules/pagekite/templates/pagekite_configure.html b/plinth/modules/pagekite/templates/pagekite_configure.html index 0ef209ee7..07c5c6066 100644 --- a/plinth/modules/pagekite/templates/pagekite_configure.html +++ b/plinth/modules/pagekite/templates/pagekite_configure.html @@ -27,20 +27,15 @@ {% include 'bootstrapform/field.html' with field=form.enabled %} -

PageKite Account

{{ form.server|bootstrap_horizontal }} {{ form.kite_name|bootstrap_horizontal }} {{ form.kite_secret|bootstrap_horizontal }} -

Services

- {{ form.http_enabled|bootstrap_horizontal }} - {{ form.ssh_enabled|bootstrap_horizontal }}
- - - + {% endblock %} @@ -52,9 +47,9 @@ $('#id_pagekite-enabled').change(function() { if ($('#id_pagekite-enabled').prop('checked')) { - $('#pagekite-post-enabled-form').show('slow'); + $('.pagekite-post-enabled-form').show('slow'); } else { - $('#pagekite-post-enabled-form').hide('slow'); + $('.pagekite-post-enabled-form').hide('slow'); } }); diff --git a/plinth/modules/pagekite/templates/pagekite_custom_services.html b/plinth/modules/pagekite/templates/pagekite_custom_services.html new file mode 100644 index 000000000..d865d9d40 --- /dev/null +++ b/plinth/modules/pagekite/templates/pagekite_custom_services.html @@ -0,0 +1,95 @@ +{% 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 %} + +{% load plinth_extras %} + +{% block page_head %} + +{% endblock %} + +{% block content %} + +

Custom Services

+ +
+ +
+

Existing custom services

+ {% if not custom_services %} + You don't have any Custom Services enabled + {% endif %} +
+ {% for service in custom_services %} + {% create_pagekite_service_link service kite_name as service_link %} +
+ + + {% if service_link|slice:":4" == "http" %} + {{ service_link }} + {% else %} + {{ service_link }} + {% endif %} + + +
+
+ {% csrf_token %} + {{ service.form }} +
+ +
+
+ {% endfor %} +
+
+ +
+
+

Create a custom service

+ {{ form|bootstrap_horizontal:'col-lg-6' }} + {% csrf_token %} + +
+
+ +
+ +{% endblock %} + + diff --git a/plinth/modules/pagekite/templates/pagekite_default_services.html b/plinth/modules/pagekite/templates/pagekite_default_services.html new file mode 100644 index 000000000..906cfb4b6 --- /dev/null +++ b/plinth/modules/pagekite/templates/pagekite_default_services.html @@ -0,0 +1,55 @@ +{% 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 page_head %} + +{% endblock %} + +{% block content %} + +

Default Services

+ +

Exposing services makes them accessible and attackable from the evil + internet. Be cautious!

+ +
+ {{ form.http|bootstrap_horizontal:'col-lg-0' }} + {{ form.https|bootstrap_horizontal:'col-lg-0' }} + {{ form.ssh|bootstrap_horizontal:'col-lg-0' }} + {% csrf_token %} + +
+ +{% endblock %} + diff --git a/plinth/modules/pagekite/templates/pagekite_introduction.html b/plinth/modules/pagekite/templates/pagekite_introduction.html index 211ff20c7..23c7fe99c 100644 --- a/plinth/modules/pagekite/templates/pagekite_introduction.html +++ b/plinth/modules/pagekite/templates/pagekite_introduction.html @@ -42,10 +42,10 @@ rest of the Internet. This includes the following situations:

PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. Currently, -exposing web server and SSH server are supported. An intermediary -server with direct Internet access is required. Currently, only -pagekite.net server is supported and you will need an account -there. In future, it might be possible to use your buddy's +exposing web server and SSH server are supported. You can use any +server that offers a pagekite service, for example +pagekite.net. +In future, it might be possible to use your buddy's {{ cfg.box_name }} for this.

diff --git a/plinth/modules/pagekite/urls.py b/plinth/modules/pagekite/urls.py index 7fa6c9918..ebc457c63 100644 --- a/plinth/modules/pagekite/urls.py +++ b/plinth/modules/pagekite/urls.py @@ -20,10 +20,19 @@ URLs for the PageKite module """ from django.conf.urls import patterns, url +from .views import DefaultServiceView, CustomServiceView, ConfigurationView, \ + DeleteServiceView urlpatterns = patterns( # pylint: disable-msg=C0103 - 'plinth.modules.pagekite.pagekite', + 'plinth.modules.pagekite.views', url(r'^apps/pagekite/$', 'index', name='index'), - url(r'^apps/pagekite/configure/$', 'configure', name='configure'), + url(r'^apps/pagekite/configure/$', ConfigurationView.as_view(), + name='configure'), + url(r'^apps/pagekite/services/default$', DefaultServiceView.as_view(), + name='default-services'), + url(r'^apps/pagekite/services/custom$', CustomServiceView.as_view(), + name='custom-services'), + url(r'^apps/pagekite/services/custom/delete$', DeleteServiceView.as_view(), + name='delete-custom-service'), ) diff --git a/plinth/modules/pagekite/util.py b/plinth/modules/pagekite/util.py new file mode 100644 index 000000000..a40ba106f --- /dev/null +++ b/plinth/modules/pagekite/util.py @@ -0,0 +1,141 @@ +# +# 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 . +# + +from gettext import gettext as _ +import logging + +from actions.pagekite_util import construct_params +from plinth import actions + +LOGGER = logging.getLogger(__name__) + +# defaults for the credentials; @kitename acts as a placeholder and is +# understood (and replaced with the actual kitename) by pagekite. +KITE_NAME = '@kitename' +KITE_SECRET = '@kitesecret' +BACKEND_HOST = 'localhost' +# predefined services show up in the PredefinedServiceForm as checkbox +PREDEFINED_SERVICES = { + 'http': { + 'params': {'protocol': 'http', + 'kitename': KITE_NAME, + 'backend_port': '80', + 'backend_host': BACKEND_HOST, + 'secret': KITE_SECRET}, + 'label': _("Web Server (HTTP)"), + 'help_text': _("Site will be available at " + "http://{0}"), + }, + 'https': { + 'params': {'protocol': 'https', + 'kitename': KITE_NAME, + 'backend_port': '443', + 'backend_host': BACKEND_HOST, + 'secret': KITE_SECRET}, + 'label': _("Web Server (HTTPS)"), + 'help_text': _("Site will be available at " + "https://{0}"), + }, + 'ssh': { + 'params': {'protocol': 'raw/22', + 'kitename': KITE_NAME, + 'backend_port': '22', + 'backend_host': BACKEND_HOST, + 'secret': KITE_SECRET}, + 'label': _("Secure Shell (SSH)"), + 'help_text': _("See SSH client setup " + "instructions") + }, +} + + +def get_kite_details(): + output = _run(['get-kite']) + kite_details = output.split() + return {'kite_name': kite_details[0], + 'kite_secret': kite_details[1]} + + +def prepare_params_for_display(params): + """Add extra information to display a custom service: + + - protocol is split into 'protocol' and 'frontend_port' + - we try to detect whether 'subdomains' are supported (as boolean) + """ + protocol = params['protocol'] + if '/' in protocol: + params['protocol'], params['frontend_port'] = protocol.split('/') + params['subdomains'] = params['kitename'].startswith('*.') + return params + + +def get_pagekite_config(): + """ + Return the current PageKite configuration by executing various actions. + """ + status = {} + + # PageKite service enabled/disabled + output = _run(['is-enabled']) + status['enabled'] = (output.split()[0] == 'yes') + + # PageKite kite details + status.update(get_kite_details()) + + # PageKite server: 'pagekite.net' if flag 'defaults' is set, + # the value of 'frontend' otherwise + use_pagekitenet_server = _run(['get-pagekitenet-frontend-status']) + if "enabled" in use_pagekitenet_server: + value = 'pagekite.net' + elif "disabled" in use_pagekitenet_server: + value = _run(['get-frontend']) + status['server'] = value.replace('\n', '') + + return status + + +def get_pagekite_services(): + """Get enabled services. Returns two values: + + 1. predefined services: {'http': False, 'ssh': True, 'https': True} + 2. custom services: [{'protocol': 'http', 'secret' 'nono', ..}, [..]} + """ + custom = [] + predefined = {} + # set all predefined services to 'disabled' by default + [predefined.update({proto: False}) for proto in PREDEFINED_SERVICES.keys()] + # now, search for the enabled ones + for serviceline in _run(['get-services']).split(): + params = construct_params(serviceline) + for name, predefined_service in PREDEFINED_SERVICES.items(): + if params == predefined_service['params']: + predefined[name] = True + break + else: + custom.append(prepare_params_for_display(params)) + return predefined, custom + + +def _run(arguments, superuser=True): + """Run a given command and raise exception if there was an error""" + command = 'pagekite' + + if superuser: + return actions.superuser_run(command, arguments) + else: + return actions.run(command, arguments) diff --git a/plinth/modules/pagekite/views.py b/plinth/modules/pagekite/views.py new file mode 100644 index 000000000..370944b03 --- /dev/null +++ b/plinth/modules/pagekite/views.py @@ -0,0 +1,123 @@ +# +# 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 . +# + +from gettext import gettext as _ +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse, reverse_lazy +from django.http.response import HttpResponseRedirect +from django.template.response import TemplateResponse +from django.views.generic import View, TemplateView +from django.views.generic.edit import FormView + +from plinth import package +from .util import get_pagekite_config, get_pagekite_services, get_kite_details +from .forms import ConfigurationForm, DefaultServiceForm, CustomServiceForm + +subsubmenu = [{'url': reverse_lazy('pagekite:index'), + 'text': _('About PageKite')}, + {'url': reverse_lazy('pagekite:configure'), + 'text': _('Configure PageKite')}, + {'url': reverse_lazy('pagekite:default-services'), + 'text': _('Default Services')}, + {'url': reverse_lazy('pagekite:custom-services'), + 'text': _('Custom Services')}] + + +@login_required +def index(request): + """Serve introduction page""" + return TemplateResponse(request, 'pagekite_introduction.html', + {'title': _('Public Visibility (PageKite)'), + 'subsubmenu': subsubmenu}) + + +class ContextMixin(object): + """Mixin to add 'subsubmenu' and 'title' to the context.""" + def get_context_data(self, **kwargs): + """Use self.title and the module-level subsubmenu""" + context = super(ContextMixin, self).get_context_data(**kwargs) + context['title'] = getattr(self, 'title', '') + context['subsubmenu'] = subsubmenu + return context + + +class DeleteServiceView(View): + def post(self, request): + form = CustomServiceForm(request.POST) + if form.is_valid(): + form.delete(request) + return HttpResponseRedirect(reverse('pagekite:custom-services')) + + +class CustomServiceView(ContextMixin, TemplateView): + template_name = 'pagekite_custom_services.html' + + def get_context_data(self, *args, **kwargs): + context = super(CustomServiceView, self).get_context_data(*args, + **kwargs) + unused, custom_services = get_pagekite_services() + for service in custom_services: + service['form'] = CustomServiceForm(initial=service) + context['custom_services'] = custom_services + context.update(get_kite_details()) + return context + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + form = CustomServiceForm(prefix="custom") + context['form'] = form + return self.render_to_response(context) + + def post(self, request): + unused, custom_services = get_pagekite_services() + form = CustomServiceForm(request.POST, prefix="custom") + if form.is_valid(): + form.save(request) + form = CustomServiceForm(prefix="custom") + + context = self.get_context_data() + context['form'] = form + + return self.render_to_response(context) + + +class DefaultServiceView(ContextMixin, FormView): + template_name = 'pagekite_default_services.html' + title = 'PageKite Default Services' + form_class = DefaultServiceForm + success_url = reverse_lazy('pagekite:default-services') + + def get_initial(self): + return get_pagekite_services()[0] + + def form_valid(self, form): + form.save(self.request) + return super(DefaultServiceView, self).form_valid(form) + + +class ConfigurationView(ContextMixin, FormView): + template_name = 'pagekite_configure.html' + form_class = ConfigurationForm + prefix = 'pagekite' + success_url = reverse_lazy('pagekite:configure') + + def get_initial(self): + return get_pagekite_config() + + def form_valid(self, form): + form.save(self.request) + return super(ConfigurationView, self).form_valid(form) diff --git a/plinth/templatetags/plinth_extras.py b/plinth/templatetags/plinth_extras.py index e3def79a1..e454a8269 100644 --- a/plinth/templatetags/plinth_extras.py +++ b/plinth/templatetags/plinth_extras.py @@ -15,6 +15,7 @@ # along with this program. If not, see . # +import copy import os from django import template @@ -58,3 +59,21 @@ def show_subsubmenu(context, menu): """Mark the active menu item and display the subsubmenu""" menu = mark_active_menuitem(menu, context['request'].path) return {'subsubmenu': menu} + + +@register.assignment_tag +def create_pagekite_service_link(service, kite_name): + """Create a link (URL) out of a pagekite service + + Parameters: - service: the params dictionary + - kite_name: kite name (from the pagekite configuration) + """ + params = {'protocol': service['protocol']} + if 'subdomains' in service and service['subdomains']: + params['kite_name'] = "*.%s" % kite_name + else: + params['kite_name'] = kite_name + link = "{protocol}://{kite_name}".format(**params) + if 'frontend_port' in service and service['frontend_port']: + link = "%s:%s" % (link, service['frontend_port']) + return link diff --git a/plinth/tests/test_pagekite_actions.py b/plinth/tests/test_pagekite_actions.py new file mode 100644 index 000000000..9f03f5fdb --- /dev/null +++ b/plinth/tests/test_pagekite_actions.py @@ -0,0 +1,79 @@ +# +# 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 . +# + +import os +import unittest + +from actions.pagekite_util import get_augeas_servicefile_path, CONF_PATH, \ + construct_params, deconstruct_params + + +class TestPagekiteActions(unittest.TestCase): + # test-cases to convert parameter-strings into param dicts and back + _tests = [ + { + 'line': 'https/8080:*.@kitename:localhost:8080:@kitesecret', + 'params': {'kitename': '*.@kitename', 'backend_host': 'localhost', + 'secret': '@kitesecret', 'protocol': 'https/8080', + 'backend_port': '8080'} + }, + { + 'line': 'https:*.@kitename:localhost:80:@kitesecret', + 'params': {'protocol': 'https', + 'kitename': '*.@kitename', + 'backend_port': '80', + 'backend_host': 'localhost', + 'secret': '@kitesecret'} + }, + { + 'line': 'raw/22:@kitename:localhost:22:@kitesecret', + 'params': {'protocol': 'raw/22', + 'kitename': '@kitename', + 'backend_port': '22', + 'backend_host': 'localhost', + 'secret': '@kitesecret'} + }, + ] + + def test_get_augeas_servicefile_path(self): + """ Test the generation of augeas-paths for pagekite services """ + tests = (('http', '80_http.rc'), + ('https', '443_https.rc'), + ('http/80', '80_http.rc'), + ('http/8080', '8080_http.rc'), + ('raw/22', '22_raw.rc')) + for protocol, filename in tests: + expected_path = os.path.join(CONF_PATH, filename, 'service_on') + returned_path = get_augeas_servicefile_path(protocol) + self.assertEqual(expected_path, returned_path) + + with self.assertRaises(ValueError): + get_augeas_servicefile_path('xmpp') + + def test_deconstruct_params(self): + """ Test deconstructing parameter dictionaries into strings """ + for test in self._tests: + self.assertEqual(test['line'], deconstruct_params(test['params'])) + + def test_construct_params(self): + """ Test constructing parameter dictionaries out of string """ + for test in self._tests: + self.assertEqual(test['params'], construct_params(test['line'])) + + line = "'https/80'; touch /etc/fstab':*.@kitename:localhost:80:foo'" + with self.assertRaises(RuntimeError): + construct_params(line)