mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-02-04 08:13:38 +00:00
refactoring pagekite: configuration form works
splitting the services to a separate page is not yet finished
This commit is contained in:
parent
cf96797040
commit
1fc0064fd0
@ -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'])
|
||||
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
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
|
||||
104
actions/pagekite_util.py
Normal file
104
actions/pagekite_util.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
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')
|
||||
@ -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)
|
||||
|
||||
169
plinth/modules/pagekite/forms.py
Normal file
169
plinth/modules/pagekite/forms.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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])
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -27,20 +27,15 @@
|
||||
|
||||
{% include 'bootstrapform/field.html' with field=form.enabled %}
|
||||
|
||||
<div id='pagekite-post-enabled-form'
|
||||
<div class='pagekite-post-enabled-form'
|
||||
style='display: {{ form.enabled.value|yesno:'block,none' }};'>
|
||||
<h3>PageKite Account</h3>
|
||||
{{ form.server|bootstrap_horizontal }}
|
||||
{{ form.kite_name|bootstrap_horizontal }}
|
||||
{{ form.kite_secret|bootstrap_horizontal }}
|
||||
|
||||
<h3>Services</h3>
|
||||
{{ form.http_enabled|bootstrap_horizontal }}
|
||||
{{ form.ssh_enabled|bootstrap_horizontal }}
|
||||
</div>
|
||||
|
||||
<input type="submit" class="btn btn-primary" value="Update setup"/>
|
||||
|
||||
<input type="submit" class="btn btn-primary" value="Save settings"/>
|
||||
</form>
|
||||
|
||||
{% 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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
|
||||
{% load plinth_extras %}
|
||||
|
||||
{% block page_head %}
|
||||
<style type="text/css">
|
||||
div.checkbox .help-block {
|
||||
display: inline-block;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
div.custom-services span.service {
|
||||
display: inline-block;
|
||||
padding-top: 6px;
|
||||
}
|
||||
input.btn {
|
||||
margin: 10px 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Custom Services</h3>
|
||||
|
||||
<div class="row custom-services">
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h4>Existing custom services</h4>
|
||||
{% if not custom_services %}
|
||||
<i>You don't have any Custom Services enabled</i>
|
||||
{% endif %}
|
||||
<div class="list-group">
|
||||
{% for service in custom_services %}
|
||||
{% create_pagekite_service_link service kite_name as service_link %}
|
||||
<div class="list-group-item clearfix">
|
||||
<span class="service">
|
||||
<span title="Forwards {{ service_link }} to {{ service.backend_host }}:{{ service.backend_port }}">
|
||||
{% if service_link|slice:":4" == "http" %}
|
||||
<a href="{{ service_link }}">{{ service_link }}</a>
|
||||
{% else %}
|
||||
{{ service_link }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</span>
|
||||
<form class="form pull-right" method="post"
|
||||
action="{% url 'pagekite:delete-custom-service' %}">
|
||||
<div style='display:none'>
|
||||
{% csrf_token %}
|
||||
{{ service.form }}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default"
|
||||
title="Delete this service">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true">
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-lg-offset-1">
|
||||
<form class="form" method="post">
|
||||
<h4>Create a custom service</h4>
|
||||
{{ form|bootstrap_horizontal:'col-lg-6' }}
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="btn btn-primary pull-right" value="Add Service"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block page_head %}
|
||||
<style type="text/css">
|
||||
div.checkbox .help-block {
|
||||
display: inline-block;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
div.custom-services span.service {
|
||||
display: inline-block;
|
||||
padding-top: 6px;
|
||||
}
|
||||
input.btn {
|
||||
margin: 10px 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Default Services</h3>
|
||||
|
||||
<p>Exposing services makes them accessible and attackable from the evil
|
||||
internet. Be cautious!</p>
|
||||
|
||||
<form class="form predefined" method="post">
|
||||
{{ form.http|bootstrap_horizontal:'col-lg-0' }}
|
||||
{{ form.https|bootstrap_horizontal:'col-lg-0' }}
|
||||
{{ form.ssh|bootstrap_horizontal:'col-lg-0' }}
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="btn btn-primary" value="Save settings"/>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -42,10 +42,10 @@ rest of the Internet. This includes the following situations: </p>
|
||||
|
||||
<p>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
|
||||
<a href="https://pagekite.net">pagekite.net</a>.
|
||||
In future, it might be possible to use your buddy's
|
||||
{{ cfg.box_name }} for this.</p>
|
||||
|
||||
<p>
|
||||
|
||||
@ -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'),
|
||||
)
|
||||
|
||||
141
plinth/modules/pagekite/util.py
Normal file
141
plinth/modules/pagekite/util.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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 "
|
||||
"<a href=\"http://{0}\">http://{0}</a>"),
|
||||
},
|
||||
'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 "
|
||||
"<a href=\"https://{0}\">https://{0}</a>"),
|
||||
},
|
||||
'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 <a href=\""
|
||||
"https://pagekite.net/wiki/Howto/SshOverPageKite/\">"
|
||||
"instructions</a>")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
123
plinth/modules/pagekite/views.py
Normal file
123
plinth/modules/pagekite/views.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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)
|
||||
@ -15,6 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
79
plinth/tests/test_pagekite_actions.py
Normal file
79
plinth/tests/test_pagekite_actions.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user