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 %}
-
-
-
-
+
{% 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 %}
+
+
+{% 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!
+
+
+
+{% 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)