diff --git a/actions/pagekite b/actions/pagekite
new file mode 100755
index 000000000..71d29e734
--- /dev/null
+++ b/actions/pagekite
@@ -0,0 +1,257 @@
+#!/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 .
+#
+
+"""
+Configuration helper for Plinth PageKite interface
+
+Unfortunately there is no python3 package for augeas yet
+"""
+
+import argparse
+import augeas
+import json
+import os
+import subprocess
+
+import util
+from pagekite_util import SERVICE_PARAMS, convert_service_to_string, \
+ get_augeas_servicefile_path, load_service, CONF_PATH
+
+aug = augeas.Augeas()
+
+PATHS = {
+ 'service_on': os.path.join(CONF_PATH, '*', 'service_on', '*'),
+ 'kitename': os.path.join(CONF_PATH, '10_account.rc', 'kitename'),
+ 'kitesecret': os.path.join(CONF_PATH, '10_account.rc', 'kitesecret'),
+ 'abort_not_configured': os.path.join(CONF_PATH, '10_account.rc',
+ 'abort_not_configured'),
+ 'defaults': os.path.join(CONF_PATH, '20_frontends.rc', 'defaults'),
+ 'frontend': os.path.join(CONF_PATH, '20_frontends.rc', 'frontend'),
+}
+
+
+def parse_arguments():
+ """Return parsed command line arguments as dictionary"""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ # Enable/disable the pagekite service
+ subparsers.add_parser('is-running', help='Get whether PakeKite is running')
+ subparsers.add_parser('start-and-enable', help='Enable PageKite service')
+ subparsers.add_parser('stop-and-disable', help='Disable PageKite service')
+ subparsers.add_parser('restart', help='Restart PageKite service')
+
+ # Frontend
+ subparsers.add_parser('get-frontend', help='Get pagekite frontend')
+ set_frontend = subparsers.add_parser('set-frontend',
+ help='Set pagekite frontend')
+ set_frontend.add_argument('url', help='frontend url')
+
+ # Kite details (name + secret)
+ subparsers.add_parser('get-kite',
+ help='Get configured kite name and secret')
+ set_kite = subparsers.add_parser('set-kite',
+ help='Configure kite name and its secret')
+ set_kite.add_argument('--kite-name',
+ help='Name of the kite (eg: mybox.pagekite.me)')
+ set_kite.add_argument('--kite-secret', help='Secret for the kite')
+
+ # Add/remove pagekite services (service_on entries)
+ subparsers.add_parser('get-services', help='Get list of enabled services')
+ add_service = subparsers.add_parser('add-service',
+ help='Add a pagekite service')
+ add_service.add_argument('--service', help='json service dictionary')
+ remove_service = subparsers.add_parser('remove-service',
+ help='Remove a pagekite service')
+ remove_service.add_argument('--service', help='json service dictionary')
+
+ return parser.parse_args()
+
+
+def _service(action):
+ """Start/stop/restart the pagekite service"""
+ error = subprocess.call(['service', 'pagekite', action])
+ if error:
+ raise Exception('Unable to %s PageKite server' % action)
+
+
+def subcommand_is_running(_):
+ """Print whether pagekite is enabled (yes or no)"""
+ print 'yes' if util.service_is_running('pagekite') else 'no'
+
+
+def subcommand_restart(_):
+ """Restart the pagekite service"""
+ _service('restart')
+ print 'restarted'
+
+
+def subcommand_start_and_enable(_):
+ aug.remove(PATHS['abort_not_configured'])
+ aug.save()
+ # 'start' alone sometimes fails, even if the service is not running
+ _service('restart')
+ print 'enabled'
+
+
+def subcommand_stop_and_disable(_):
+ _service('stop')
+ aug.set(PATHS['abort_not_configured'], '')
+ aug.save()
+ print 'disabled'
+
+
+def subcommand_get_frontend(_):
+ """Get pagekite frontend url"""
+ if aug.match(PATHS['defaults']):
+ print "pagekite.net"
+ else:
+ url = aug.get(PATHS['frontend'])
+ print url if url else ""
+
+
+def subcommand_set_frontend(arguments):
+ """Set pagekite frontend url, taking care of defaults and pagekite.net"""
+ if arguments.url in ('pagekite.net', 'defaults', 'default'):
+ enable_pagekitenet_frontend()
+ else:
+ aug.remove(PATHS['defaults'])
+ aug.set(PATHS['frontend'], arguments.url)
+ aug.save()
+
+
+def enable_pagekitenet_frontend():
+ """Enable using default pageket.net frontend
+
+ This disables any other frontends.
+ """
+ aug.set(PATHS['defaults'], '')
+ aug.remove(PATHS['frontend'])
+ aug.save()
+ print "enabled"
+
+
+def subcommand_get_services(arguments):
+ """ lists all available (enabled) services """
+ for match in aug.match(PATHS['service_on']):
+ service = dict([(param, aug.get(os.path.join(match, param)))
+ for param in SERVICE_PARAMS])
+ print json.dumps(service)
+
+
+def subcommand_remove_service(arguments):
+ """Searches and removes the service(s) that match all given parameters"""
+ service = load_service(arguments.service)
+ paths = get_existing_service_paths(service)
+ # TODO: theoretically, everything to do here is:
+ # [aug.remove(path) for path in paths]
+ # but augeas won't let you save the changed files and doesn't say why
+ for path in paths:
+ filepath = convert_augeas_path_to_filepath(path)
+ service_found = False
+ with open(filepath, 'r') as file:
+ lines = file.readlines()
+ for i, line in enumerate(lines):
+ if line.startswith('service_on') and \
+ all(param in line for param in service.values()):
+ lines[i] = ""
+ service_found = True
+ break
+ if service_found:
+ with open(filepath, 'w') as file:
+ file.writelines(lines)
+ # abort to only allow deleting one service
+ break
+ _service('restart')
+
+
+def get_existing_service_paths(service):
+ """Return paths of existing services that match the given service params"""
+ # construct an augeas query path with patterns like:
+ # */service_on/*[protocol='http']
+ path = PATHS['service_on']
+ for param, value in service.items():
+ path += "[%s='%s']" % (param, value)
+ return aug.match(path)
+
+
+def subcommand_add_service(arguments):
+ """Add one service"""
+ service = load_service(arguments.service)
+ if get_existing_service_paths(service):
+ msg = "Service with the parameters %s already exists"
+ raise RuntimeError(msg % service)
+
+ root = get_new_service_path(service['protocol'])
+ # TODO: after adding a service, augeas fails writing the config;
+ # so add the service_on entry manually instead
+ path = convert_augeas_path_to_filepath(root)
+ with open(path, 'a') as servicefile:
+ line = "\nservice_on = %s\n" % convert_service_to_string(service)
+ servicefile.write(line)
+ _service('restart')
+
+
+def convert_augeas_path_to_filepath(augpath, prefix='/files',
+ suffix='service_on'):
+ """Convert an augeas service_on path to the actual file path"""
+ if augpath.startswith(prefix):
+ augpath = augpath.replace(prefix, "", 1)
+
+ index = augpath.rfind(suffix)
+ if index:
+ augpath = augpath[:index]
+ return augpath.rstrip('/')
+
+
+def get_new_service_path(protocol):
+ """Get the augeas path of a new service for a protocol
+
+ This takes care of existing services using a /service_on/*/ query"""
+ root = get_augeas_servicefile_path(protocol)
+ new_index = len(aug.match(root + '/*')) + 1
+ return os.path.join(root, str(new_index))
+
+
+def subcommand_get_kite(_):
+ """Print details of the currently configured kite"""
+ kitename = aug.get(PATHS['kitename'])
+ kitesecret = aug.get(PATHS['kitesecret'])
+ print kitename if kitename else ''
+ print kitesecret if kitesecret else ''
+
+
+def subcommand_set_kite(arguments):
+ """Set details of the kite"""
+ aug.set(PATHS['kitename'], arguments.kite_name)
+ aug.set(PATHS['kitesecret'], arguments.kite_secret)
+ aug.save()
+
+
+def main():
+ """Parse arguments and perform all duties"""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/actions/pagekite-configure b/actions/pagekite-configure
deleted file mode 100755
index a5af96fb0..000000000
--- a/actions/pagekite-configure
+++ /dev/null
@@ -1,290 +0,0 @@
-#!/usr/bin/python3
-# -*- 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 .
-#
-
-"""
-Configuration helper for Plint PageKite inteface
-
-TODO: Use augeas for manipulating /etc/pagekite.d/* files
-"""
-
-# Disable warning about invalid module name # pylint: disable-msg=C0103
-
-import argparse
-import os
-import re
-import subprocess
-
-CONFIG_DIR = '/etc/pagekite.d'
-
-SERVICE_FILE_MAP = {
- 'http': {
- 'file': '80_httpd.rc',
- 'match': 'http:',
- 'line': 'service_on = http:@kitename : localhost:80 : @kitesecret'},
- 'ssh': {
- 'file': '80_sshd.rc',
- 'match': 'raw/22:',
- 'line': 'service_on = raw/22:@kitename : localhost:22 : @kitesecret'}}
-
-
-def parse_arguments():
- """Return parsed command line arguments as dictionary"""
- parser = argparse.ArgumentParser()
- subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
-
- # Start PageKite
- subparsers.add_parser('start', help='Start PageKite service')
-
- # Stop PageKite
- subparsers.add_parser('stop', help='Stop PageKite service')
-
- # Get status
- subparsers.add_parser('get-status', help='Get whether PakeKite is enabled')
-
- # Set status
- set_status = subparsers.add_parser('set-status',
- help='Enable/disable PageKite')
- set_status.add_argument('enable', choices=['enable', 'disable'])
-
- # Get kite details
- subparsers.add_parser('get-kite',
- help='Get configured kite name and secret')
-
- # Set kite details
- set_kite = subparsers.add_parser('set-kite',
- help='Configure kite name and its secret')
- set_kite.add_argument('--kite-name',
- help='Name of the kite (eg: mybox.pagekite.me)')
- set_kite.add_argument('--kite-secret', help='Secret for the kite')
-
- # Get service status
- get_service = subparsers.add_parser('get-service-status',
- help='Get whether service is enabled')
- get_service.add_argument('service', choices=['http', 'ssh'])
-
- # Set service status
- set_service = subparsers.add_parser('set-service-status',
- help='Enable/disable a service')
- set_service.add_argument('service', choices=['http', 'ssh'])
- set_service.add_argument('enable', choices=['enable', 'disable'])
-
- return parser.parse_args()
-
-
-def subcommand_start(_):
- """Start PageKite service"""
- status = subprocess.call(['service', 'pagekite', 'start'])
- if status:
- raise Exception('Unable to start PageKite server')
-
-
-def subcommand_stop(_):
- """Stop PageKite service"""
- status = subprocess.call(['service', 'pagekite', 'stop'])
- if status:
- raise Exception('Unable to stop PageKite server')
-
-
-def subcommand_get_status(_):
- """Print status of the pagekite service"""
- is_enabled = is_pagekite_enabled()
- print('enabled' if is_enabled else 'disabled')
-
-
-def is_pagekite_enabled():
- """Return if pagekite is enabled"""
- service_file_path = os.path.join(CONFIG_DIR, '10_account.rc')
- try:
- with open(service_file_path, 'r') as file_object:
- for line in file_object:
- regex = r'^[ \t]*abort_not_configured'
- if re.match(regex, line):
- return False
- except Exception:
- return True
-
- return True
-
-
-def subcommand_set_status(arguments):
- """Enable/disable the pagekite service"""
- enable = arguments.enable == 'enable'
- is_enabled = is_pagekite_enabled()
- if enable and is_enabled:
- print('already enabled')
- return
-
- if not enable and not is_enabled:
- print('already disabled')
- return
-
- if enable:
- pagekite_enable()
- print('enabled')
- else:
- pagekite_disable()
- print('disabled')
-
-
-def pagekite_enable():
- """Enable the pagekite daemon"""
- file_path = os.path.join(CONFIG_DIR, '10_account.rc')
- file_path_new = os.path.join(CONFIG_DIR, '10_account.rc.new')
- with open(file_path, 'r') as read_file_object, \
- open(file_path_new, 'w') as write_file_object:
- for line in read_file_object:
- if not re.match('^[ \t]*abort_not_configured', line):
- write_file_object.write(line)
-
- os.rename(file_path_new, file_path)
-
-
-def pagekite_disable():
- """Disable the pagekite daemon"""
- file_path = os.path.join(CONFIG_DIR, '10_account.rc')
- with open(file_path, 'a') as file_object:
- file_object.write('abort_not_configured\n')
-
-
-def subcommand_get_kite(_):
- """Print details of the currently configure kite"""
- kite_name = ''
- kite_secret = ''
-
- file_path = os.path.join(CONFIG_DIR, '10_account.rc')
- with open(file_path, 'r') as file_object:
- for line in file_object:
- match = re.match(r'[ \t]*kitename[ \t]*=[ \t]*(.*)', line)
- if match:
- kite_name = match.group(1)
- continue
-
- match = re.match(r'[ \t]*kitesecret[ \t]*=[ \t]*(.*)', line)
- if match:
- kite_secret = match.group(1)
- continue
-
- print(kite_name)
- print(kite_secret)
-
-
-def subcommand_set_kite(arguments):
- """Set details of the kite"""
- kite_name = arguments.kite_name
- kite_secret = arguments.kite_secret
-
- file_path = os.path.join(CONFIG_DIR, '10_account.rc')
- file_path_new = os.path.join(CONFIG_DIR, '10_account.rc.new')
- with open(file_path, 'r') as read_file_object, \
- os.fdopen(os.open(file_path_new, os.O_WRONLY | os.O_CREAT,
- 0o400), 'w') as write_file_object:
- for line in read_file_object:
- if re.match(r'[ \t]*kitename[ \t]*=.*', line):
- write_file_object.write(
- 'kitename = {kite_name}\n'.format(kite_name=kite_name))
- continue
-
- if re.match(r'[ \t]*kitesecret[ \t]*=.*', line):
- write_file_object.write('kitesecret = {kite_secret}\n'
- .format(kite_secret=kite_secret))
- continue
-
- write_file_object.write(line)
-
- os.rename(file_path_new, file_path)
-
-
-def subcommand_get_service_status(arguments):
- """Print status of the pagekite service"""
- is_enabled = is_service_enabled(arguments.service)
- print('enabled' if is_enabled else 'disabled')
-
-
-def is_service_enabled(service):
- """Return if a service is enabled"""
- service = SERVICE_FILE_MAP[service]
- service_file_path = os.path.join(CONFIG_DIR, service['file'])
- if not os.path.isfile(service_file_path):
- return False
-
- try:
- with open(service_file_path, 'r') as file_object:
- for line in file_object:
- regex = '[ \t]*service_on[ \t]*=[ \t]*{match}'
- regex = regex.format(match=service['match'])
- if re.match(regex, line):
- return True
- except Exception:
- return False
-
- return False
-
-
-def subcommand_set_service_status(arguments):
- """Enable/disable a pagekite service"""
- enable = arguments.enable == 'enable'
- is_enabled = is_service_enabled(arguments.service)
- if enable and is_enabled:
- print('already enabled')
- return
-
- if not enable and not is_enabled:
- print('already disabled')
- return
-
- if enable:
- service_enable(arguments.service)
- print('enabled')
- else:
- service_disable(arguments.service)
- print('disabled')
-
-
-def service_enable(service_name):
- """Enable a service"""
- service = SERVICE_FILE_MAP[service_name]
- service_file_path = os.path.join(CONFIG_DIR, service['file'])
- with open(service_file_path, 'w') as file_object:
- file_object.write('''
-# Expose the local {service_name} daemon
-# File auto-generated by Plinth
-
-{line}
-'''.format(service_name=service_name, line=service['line']))
-
-
-def service_disable(service_name):
- """Disable a service"""
- service = SERVICE_FILE_MAP[service_name]
- service_file_path = os.path.join(CONFIG_DIR, service['file'])
- service_file_path_new = os.path.join(CONFIG_DIR,
- service['file'] + '.plinthbak')
- os.rename(service_file_path, service_file_path_new)
-
-
-def main():
- """Parse arguments and perform all duties"""
- arguments = parse_arguments()
-
- subcommand = arguments.subcommand.replace('-', '_')
- subcommand_method = globals()['subcommand_' + subcommand]
- subcommand_method(arguments)
-
-if __name__ == "__main__":
- main()
diff --git a/actions/pagekite_util.py b/actions/pagekite_util.py
new file mode 100644
index 000000000..26b8c757f
--- /dev/null
+++ b/actions/pagekite_util.py
@@ -0,0 +1,113 @@
+#!/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 .
+#
+
+"""
+Utilities for configuring PageKite.
+"""
+# TODO:
+# Once python-augeas is available for python3 import the following things
+# from plinth.modules.pagekite.util (instead of having a copy in here):
+#
+# SERVICE_PARAMS, convert_service_to_string
+#
+# until then, this file is python2 and python3 compatible for the unittests
+
+import os
+import json
+
+CONF_PATH = '/files/etc/pagekite.d'
+
+# parameters that get stored in configuration service_on entries
+SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port',
+ 'secret']
+
+
+def convert_service_to_string(service):
+ """ Convert service dict into a ":"-separated parameter string
+
+ >>> convert_service_to_string({'kitename': '@kitename', \
+'backend_host': 'localhost', 'secret': '@kitesecret', \
+'protocol': 'https/443', 'backend_port': '443'})
+ 'https/443:@kitename:localhost:443:@kitesecret'
+ """
+ try:
+ service_string = ":".join([service[param] for param in SERVICE_PARAMS])
+ except KeyError:
+ raise ValueError("Could not parse params: %s " % service)
+ return service_string
+
+
+def load_service(json_service):
+ """ create a service out of json command-line argument
+
+ 1) parse json
+ 2) only use the parameters that we need (SERVICE_PARAMS)
+ 3) convert unicode to strings
+ """
+ service = json.loads(json_service)
+ return dict((str(key), str(service[key])) for key in SERVICE_PARAMS)
+
+
+def get_augeas_servicefile_path(protocol):
+ """Get the augeas path where a service for a protocol should be stored
+
+ TODO: Once we use python3 switch from doctests to unittests
+
+ >>> get_augeas_servicefile_path('http')
+ '/files/etc/pagekite.d/80_http.rc/service_on'
+
+ >>> get_augeas_servicefile_path('https')
+ '/files/etc/pagekite.d/443_https.rc/service_on'
+
+ >>> get_augeas_servicefile_path('http/80')
+ '/files/etc/pagekite.d/80_http.rc/service_on'
+
+ >>> get_augeas_servicefile_path('http/8080')
+ '/files/etc/pagekite.d/8080_http.rc/service_on'
+
+ >>> get_augeas_servicefile_path('raw/22')
+ '/files/etc/pagekite.d/22_raw.rc/service_on'
+
+ >>> get_augeas_servicefile_path('xmpp')
+ Traceback (most recent call last):
+ ...
+ ValueError: Unsupported protocol: xmpp
+
+ """
+ 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')
+
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/actions/tor b/actions/tor
index f72c7fd5e..5a546fd4b 100755
--- a/actions/tor
+++ b/actions/tor
@@ -25,6 +25,8 @@ import argparse
import os
import subprocess
+import util
+
SERVICE_CONFIG = '/etc/default/tor'
TOR_CONFIG = '/etc/tor/torrc'
@@ -57,20 +59,7 @@ def parse_arguments():
def subcommand_is_running(_):
"""Get whether Tor is running"""
- try:
- output = subprocess.check_output(['service', 'tor', 'status'])
- except subprocess.CalledProcessError:
- # If Tor is not running we get a status code != 0 and a
- # CalledProcessError
- print('no')
- else:
- running = False
- for line in output.decode().split('\n'):
- if 'Active' in line and 'running' in line:
- running = True
- break
-
- print('yes' if running else 'no')
+ print('yes' if util.service_is_running('tor') else 'no')
def subcommand_start(_):
diff --git a/actions/util.py b/actions/util.py
new file mode 100644
index 000000000..1f097f09d
--- /dev/null
+++ b/actions/util.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python3
+# -*- 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 .
+#
+
+"""
+Python action utility functions
+"""
+
+import subprocess
+
+
+def service_is_running(servicename):
+ """Evaluates whether a service is currently running. Returns boolean"""
+ try:
+ output = subprocess.check_output(['service', servicename, 'status'])
+ except subprocess.CalledProcessError:
+ # Usually if a service is not running we get a status code != 0 and
+ # thus a CalledProcessError
+ return False
+ else:
+ running = False # default value
+ for line in output.decode('utf-8').split('\n'):
+ if 'Active' in line and 'running' in line:
+ running = True
+ break
+ return running
diff --git a/data/usr/share/augeas/lenses/pagekite.aug b/data/usr/share/augeas/lenses/pagekite.aug
new file mode 100644
index 000000000..83ced80bd
--- /dev/null
+++ b/data/usr/share/augeas/lenses/pagekite.aug
@@ -0,0 +1,79 @@
+(*
+Module: Pagekite
+ Parses /etc/pagekite.d/
+
+Author: Michael Pimmer
+
+About: License
+ This file is licenced under the LGPL v2+, like the rest of Augeas.
+*)
+
+module Pagekite =
+autoload xfm
+
+(* View: lns *)
+
+(* Variables *)
+let equals = del /[ \t]*=[ \t]*/ "="
+let neg2 = /[^# \n\t]+/
+let neg3 = /[^# \:\n\t]+/
+let eol = del /\n/ "\n"
+(* Match everything from here to eol, cropping whitespace at both ends *)
+let to_eol = /[^ \t\n](.*[^ \t\n])?/
+
+(* A key followed by comma-separated values
+ k: name of the key
+ key_sep: separator between key and values
+ value_sep: separator between values
+ sto: store for values
+*)
+let key_csv_line (k:string) (key_sep:lens) (value_sep:lens) (sto:lens) =
+ [ key k . key_sep . [ seq k . sto ] .
+ [ seq k . value_sep . sto ]* . Util.eol ]
+
+(* entries for pagekite.d/10_account.rc *)
+let domain = [ key "domain" . equals . store neg2 . Util.comment_or_eol ]
+let frontend = Build.key_value_line ("frontend" | "frontends")
+ equals (store Rx.neg1)
+let host = Build.key_value_line "host" equals (store Rx.ip)
+let ports = key_csv_line "ports" equals Sep.comma (store Rx.integer)
+let protos = key_csv_line "protos" equals Sep.comma (store Rx.word)
+
+(* entries for pagekite.d/20_frontends.rc *)
+let kitesecret = Build.key_value_line "kitesecret" equals (store Rx.space_in)
+let kv_frontend = Build.key_value_line ( "kitename" | "fe_certname" |
+ "ca_certs" | "tls_endpoint" )
+ equals (store Rx.neg1)
+
+(* entries for services like 80_httpd.rc *)
+let service_colon = del /[ \t]*:[ \t]*/ " : "
+let service_on = [ key "service_on" . [ seq "service_on" . equals .
+ [ label "protocol" . store neg3 ] . service_colon .
+ [ label "kitename" . (store neg3) ] . service_colon .
+ [ label "backend_host" . (store neg3) ] . service_colon .
+ [ label "backend_port" . (store neg3) ] . service_colon . (
+ [ label "secret" . (store Rx.no_spaces) . Util.eol ] | eol
+ ) ] ]
+
+let service_cfg = [ key "service_cfg" . equals . store to_eol . eol ]
+
+let flags = ( "defaults" | "isfrontend" | "abort_not_configured" | "insecure" )
+
+let entries = Build.flag_line flags
+ | domain
+ | frontend
+ | host
+ | ports
+ | protos
+ | kv_frontend
+ | kitesecret
+ | service_on
+ | service_cfg
+
+let lns = ( entries | Util.empty | Util.comment )*
+
+(* View: filter *)
+let filter = incl "/etc/pagekite.d/*.rc"
+ . Util.stdexcl
+
+let xfm = transform lns filter
diff --git a/data/usr/share/augeas/lenses/tests/test_pagekite.aug b/data/usr/share/augeas/lenses/tests/test_pagekite.aug
new file mode 100644
index 000000000..d32bc4f48
--- /dev/null
+++ b/data/usr/share/augeas/lenses/tests/test_pagekite.aug
@@ -0,0 +1,111 @@
+module Test_Pagekite =
+
+let conf1 = "# Use the pagekite.net service defaults.
+defaults
+"
+test Pagekite.lns get conf1 =
+ { "#comment" = "Use the pagekite.net service defaults." }
+ { "defaults" }
+
+
+let conf2 ="
+frontends = pagekite.freedombox.me
+ports=80,81
+"
+test Pagekite.lns get conf2 =
+ { }
+ { "frontends" = "pagekite.freedombox.me" }
+ { "ports"
+ { "1" = "80" }
+ { "2" = "81" } }
+
+
+let conf3 = "frontend=pagekite.freedombox.me
+host=192.168.0.3
+"
+test Pagekite.lns get conf3 =
+ { "frontend" = "pagekite.freedombox.me" }
+ { "host" = "192.168.0.3" }
+
+
+let conf4 = "isfrontend
+ports=80,443
+protos=http,https
+domain=http,https:*.your.domain:MakeUpAPasswordHere
+"
+test Pagekite.lns get conf4 =
+ { "isfrontend" }
+ { "ports"
+ { "1" = "80" }
+ { "2" = "443" } }
+ { "protos"
+ { "1" = "http" }
+ { "2" = "https" } }
+ { "domain" = "http,https:*.your.domain:MakeUpAPasswordHere" }
+
+let conf_account = "kitename = my.freedombox.me
+kitesecret = 0420
+# Delete this line!
+abort_not_configured
+"
+test Pagekite.lns get conf_account =
+ { "kitename" = "my.freedombox.me" }
+ { "kitesecret" = "0420" }
+ { "#comment" = "Delete this line!" }
+ { "abort_not_configured" }
+
+
+let conf_service = "
+service_on = raw/22:@kitename : localhost:22 : @kitesecret
+service_on=http:192.168.0.1:127.0.0.1:80:
+service_on=https:yourhostname,fqdn:127.0.0.1:443:
+"
+test Pagekite.lns get conf_service =
+ { }
+ { "service_on"
+ { "1"
+ { "protocol" = "raw/22" }
+ { "kitename" = "@kitename" }
+ { "backend_host" = "localhost" }
+ { "backend_port" = "22" }
+ { "secret" = "@kitesecret" }
+ }
+ }
+ { "service_on"
+ { "2"
+ { "protocol" = "http" }
+ { "kitename" = "192.168.0.1" }
+ { "backend_host" = "127.0.0.1" }
+ { "backend_port" = "80" }
+ }
+ }
+ { "service_on"
+ { "3"
+ { "protocol" = "https" }
+ { "kitename" = "yourhostname,fqdn" }
+ { "backend_host" = "127.0.0.1" }
+ { "backend_port" = "443" }
+ }
+ }
+
+
+let conf_encryption = "
+frontend=frontend.your.domain:443
+fe_certname=frontend.your/domain
+ca_certs=/etc/pagekite.d/site-cert.pem
+tls_endpoint=frontend.your.domain:/path/to/frontend.pem
+"
+test Pagekite.lns get conf_encryption =
+ { }
+ { "frontend" = "frontend.your.domain:443" }
+ { "fe_certname" = "frontend.your/domain" }
+ { "ca_certs" = "/etc/pagekite.d/site-cert.pem" }
+ { "tls_endpoint" = "frontend.your.domain:/path/to/frontend.pem" }
+
+
+let conf_service_cfg = "insecure
+service_cfg = KITENAME.pagekite.me/80 : insecure : True
+"
+test Pagekite.lns get conf_service_cfg =
+ { "insecure" }
+ { "service_cfg" = "KITENAME.pagekite.me/80 : insecure : True" }
diff --git a/plinth/modules/__init__.py b/plinth/modules/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/modules/pagekite/__init__.py b/plinth/modules/pagekite/__init__.py
index 0d836a04f..18fcccbcb 100644
--- a/plinth/modules/pagekite/__init__.py
+++ b/plinth/modules/pagekite/__init__.py
@@ -19,9 +19,16 @@
Plinth module to configure PageKite
"""
-from . import pagekite
-from .pagekite import init
+from gettext import gettext as _
+from plinth import cfg
-__all__ = ['pagekite', 'init']
+__all__ = ['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..ea746f0eb
--- /dev/null
+++ b/plinth/modules/pagekite/forms.py
@@ -0,0 +1,224 @@
+#
+# 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 copy
+from gettext import gettext as _
+import json
+import logging
+
+from django import forms
+from django.contrib import messages
+from django.core import validators
+
+from plinth.errors import ActionError
+from .util import _run, get_kite_details, BACKEND_HOST, KITE_NAME, \
+ KITE_SECRET, PREDEFINED_SERVICES
+
+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: mybox.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:
+
+ config_changed = False
+
+ 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'))
+ config_changed = True
+
+ if old['server'] != new['server']:
+ _run(['set-frontend', new['server']])
+ messages.success(request, _('Pagekite server set'))
+ config_changed = True
+
+ if old['enabled'] != new['enabled']:
+ if new['enabled']:
+ _run(['start-and-enable'])
+ messages.success(request, _('PageKite enabled'))
+ else:
+ _run(['stop-and-disable'])
+ messages.success(request, _('PageKite disabled'))
+
+ # Restart the service if the config was changed while the service
+ # was running, so changes take effect immediately.
+ elif config_changed and new['enabled']:
+ _run(['restart'])
+
+
+class StandardServiceForm(forms.Form):
+ """Creates a form out of PREDEFINED_SERVICES"""
+
+ def __init__(self, *args, **kwargs):
+ """Add the fields from PREDEFINED_SERVICES"""
+ super(StandardServiceForm, 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_name in PREDEFINED_SERVICES.keys():
+ if self.initial[service_name] != formdata[service_name]:
+ service = PREDEFINED_SERVICES[service_name]['params']
+ service = json.dumps(service)
+ if formdata[service_name]:
+ _run(['add-service', '--service', service])
+ messages.success(request, _('Service enabled: {name}')
+ .format(name=service_name))
+ else:
+ _run(['remove-service', '--service', service])
+ messages.success(request, _('Service disabled: {name}')
+ .format(name=service_name))
+
+
+class BaseCustomServiceForm(forms.Form):
+ """Basic form functionality to handle 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",
+ required=True)
+ backend_port = forms.IntegerField(min_value=0, max_value=65535,
+ label="internal (freedombox) port")
+ subdomains = forms.BooleanField(label="Enable Subdomains", required=False)
+
+ def convert_formdata_to_service(self, formdata):
+ """Add information to make a service out of the form data"""
+ # convert integers to str (to compare values with DEFAULT_SERVICES)
+ for field in ('frontend_port', 'backend_port'):
+ formdata[field] = str(formdata[field])
+
+ # set kitename and kitesecret if not already set
+ if 'kitename' not in formdata:
+ if 'subdomains' in formdata and formdata['subdomains']:
+ formdata['kitename'] = "*.%s" % KITE_NAME
+ else:
+ formdata['kitename'] = KITE_NAME
+ if 'secret' not in formdata:
+ formdata['secret'] = KITE_SECRET
+
+ # merge protocol and frontend_port back to one entry (protocol)
+ if 'frontend_port' in formdata:
+ if formdata['frontend_port'] not in formdata['protocol']:
+ formdata['protocol'] = "%s/%s" % (formdata['protocol'],
+ formdata['frontend_port'])
+ if 'backend_host' not in formdata:
+ formdata['backend_host'] = BACKEND_HOST
+
+ return formdata
+
+
+class DeleteCustomServiceForm(BaseCustomServiceForm):
+
+ def delete(self, request):
+ service = self.convert_formdata_to_service(self.cleaned_data)
+ _run(['remove-service', '--service', json.dumps(service)])
+ messages.success(request, _('Deleted custom service'))
+
+
+class AddCustomServiceForm(BaseCustomServiceForm):
+ """Adds the save() method and validation to not add predefined services"""
+
+ def matches_predefined_service(self, formdata):
+ """Returns whether the user input matches a predefined service"""
+ service = self.convert_formdata_to_service(formdata)
+ match_found = False
+ for predefined_service_obj in PREDEFINED_SERVICES.values():
+ # manually add the port to compare predefined with custom services
+ # that's due to the (sometimes) implicit port in the configuration
+ predefined_service = copy.copy(predefined_service_obj['params'])
+ if predefined_service['protocol'] == 'http':
+ predefined_service['protocol'] = 'http/80'
+ elif predefined_service['protocol'] == 'https':
+ predefined_service['protocol'] = 'https/443'
+
+ # The formdata service has additional keys, so we can't compare
+ # the dicts directly.
+ # instead look whether predefined_service is a subset of service
+ if all(service[k] == v for k, v in predefined_service.items()):
+ match_found = True
+ break
+ return match_found
+
+ def clean(self):
+ cleaned_data = super(AddCustomServiceForm, self).clean()
+ try:
+ is_predefined = self.matches_predefined_service(cleaned_data)
+ except KeyError:
+ is_predefined = False
+ if is_predefined:
+ msg = _("""This service is available as a standard service. Please
+ use the 'Standard Services' page to enable it.""")
+ raise forms.ValidationError(msg)
+ return cleaned_data
+
+ def save(self, request):
+ service = self.convert_formdata_to_service(self.cleaned_data)
+ try:
+ _run(['add-service', '--service', json.dumps(service)])
+ messages.success(request, _('Added custom service'))
+ except ActionError as exception:
+ if "already exists" in str(exception):
+ messages.error(request, _('This service already exists'))
+ else:
+ raise
diff --git a/plinth/modules/pagekite/pagekite.py b/plinth/modules/pagekite/pagekite.py
deleted file mode 100644
index 132d7fff7..000000000
--- a/plinth/modules/pagekite/pagekite.py
+++ /dev/null
@@ -1,199 +0,0 @@
-#
-# 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 .
-#
-
-"""
-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)
-
-
-class ConfigureForm(forms.Form): # pylint: disable-msg=W0232
- """Form to configure PageKite"""
- enabled = forms.BooleanField(label=_('Enable PageKite'),
- required=False)
-
- server = forms.CharField(
- label=_('Server'), required=False,
- help_text=_('Currently only pagekite.net server is supported'),
- widget=forms.TextInput(attrs={'placeholder': 'pagekite.net',
- 'disabled': 'disabled'}))
-
- 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'))
-
- http_enabled = forms.BooleanField(
- label=_('Web Server (HTTP)'), required=False,
- help_text=_('Site will be available at \
-http://mybox1-myacc.pagekite.me \
-'))
-
- ssh_enabled = forms.BooleanField(
- label=_('Secure Shell (SSH)'), required=False,
- help_text=_('See SSH client setup instructions'))
-
-
-@login_required
-@package.required(['pagekite'])
-def configure(request):
- """Serve the configuration form"""
- status = get_status()
-
- form = None
-
- if request.method == 'POST':
- form = ConfigureForm(request.POST, prefix='pagekite')
- # pylint: disable-msg=E1101
- if form.is_valid():
- _apply_changes(request, status, form.cleaned_data)
- status = get_status()
- form = ConfigureForm(initial=status, prefix='pagekite')
- else:
- form = ConfigureForm(initial=status, prefix='pagekite')
-
- return TemplateResponse(request, 'pagekite_configure.html',
- {'title': _('Configure PageKite'),
- 'status': status,
- 'form': form,
- 'subsubmenu': subsubmenu})
-
-
-def get_status():
- """
- Return the current status of PageKite configuration by
- executing various actions.
- """
- status = {}
-
- # PageKite service enabled/disabled
- output = _run(['get-status'])
- status['enabled'] = (output.split()[0] == 'enabled')
-
- # PageKite kite details
- output = _run(['get-kite'])
- kite_details = output.split()
- status['kite_name'] = kite_details[0]
- status['kite_secret'] = kite_details[1]
-
- # Service status
- status['service'] = {}
- for service in ('http', 'ssh'):
- output = _run(['get-service-status', service])
- status[service + '_enabled'] = (output.split()[0] == 'enabled')
-
- return status
-
-
-def _apply_changes(request, old_status, new_status):
- """Apply the changes to PageKite configuration"""
- LOGGER.info('New status is - %s', new_status)
-
- if old_status != new_status:
- _run(['stop'])
-
- if old_status['enabled'] != new_status['enabled']:
- if new_status['enabled']:
- _run(['set-status', 'enable'])
- messages.success(request, _('PageKite enabled'))
- else:
- _run(['set-status', 'disable'])
- messages.success(request, _('PageKite disabled'))
-
- if old_status['kite_name'] != new_status['kite_name'] or \
- old_status['kite_secret'] != new_status['kite_secret']:
- _run(['set-kite', '--kite-name', new_status['kite_name'],
- '--kite-secret', new_status['kite_secret']])
- messages.success(request, _('Kite details set'))
-
- for service in ['http', 'ssh']:
- if old_status[service + '_enabled'] != \
- new_status[service + '_enabled']:
- if new_status[service + '_enabled']:
- _run(['set-service-status', service, 'enable'])
- messages.success(request, _('Service enabled: {service}')
- .format(service=service))
- else:
- _run(['set-service-status', service, 'disable'])
- messages.success(request, _('Service disabled: {service}')
- .format(service=service))
-
- if old_status != new_status:
- _run(['start'])
-
-
-def _run(arguments, superuser=True):
- """Run a given command and raise exception if there was an error"""
- command = 'pagekite-configure'
-
- if superuser:
- return actions.superuser_run(command, arguments)
- else:
- return actions.run(command, arguments)
diff --git a/plinth/modules/pagekite/templates/pagekite_configure.html b/plinth/modules/pagekite/templates/pagekite_configure.html
index 0d737ba9b..8276a8994 100644
--- a/plinth/modules/pagekite/templates/pagekite_configure.html
+++ b/plinth/modules/pagekite/templates/pagekite_configure.html
@@ -30,17 +30,12 @@
PageKite Account
- {% include 'bootstrapform/field.html' with field=form.server %}
- {% include 'bootstrapform/field.html' with field=form.kite_name %}
- {% include 'bootstrapform/field.html' with field=form.kite_secret %}
+ {{ form.server|bootstrap_horizontal }}
+ {{ form.kite_name|bootstrap_horizontal }}
+ {{ form.kite_secret|bootstrap_horizontal }}
-
Services
- {% include 'bootstrapform/field.html' with field=form.http_enabled %}
- {% include 'bootstrapform/field.html' with field=form.ssh_enabled %}
-
-
-
+
{% endblock %}
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..5efec6ef8
--- /dev/null
+++ b/plinth/modules/pagekite/templates/pagekite_custom_services.html
@@ -0,0 +1,104 @@
+{% 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 pagekite_extras %}
+
+{% block page_head %}
+
+{% endblock %}
+
+{% block content %}
+
+
+ Warning: Your PageKite frontend server may not support all the
+ protocol/port combinations that you are able to define here. For example,
+ HTTPS on ports other than 443 is known to cause problems.
+
+
+
+
+
+
+
+
+
+
Existing custom services
+ {% if not custom_services %}
+ You don't have any Custom Services enabled
+ {% endif %}
+
+ {% for service in custom_services %}
+ {% create_pagekite_service_url service kite_name as service_url %}
+
PageKite is a system for exposing {{ cfg.box_name }} services when
you don't have a direct connection to the Internet. You only need this
-service if your {{ cfg.box_name }} services are unreachable from the
+if your {{ cfg.box_name }} services are unreachable from the
rest of the Internet. This includes the following situations:
@@ -41,11 +41,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
+using a combination of tunnels and reverse proxies. You can use any
+pagekite service provider, 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/templates/pagekite_standard_services.html b/plinth/modules/pagekite/templates/pagekite_standard_services.html
new file mode 100644
index 000000000..53fbd99e4
--- /dev/null
+++ b/plinth/modules/pagekite/templates/pagekite_standard_services.html
@@ -0,0 +1,52 @@
+{% 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 %}
+
+
+ Warning:
+
Published services are accessible and attackable from the evil internet.
+
Exposing SSH with the default password for 'fbx' is a VERY BAD idea.
+
+
+
+
+{% endblock %}
+
diff --git a/plinth/modules/pagekite/templatetags/pagekite_extras.py b/plinth/modules/pagekite/templatetags/pagekite_extras.py
new file mode 100644
index 000000000..c6ce5ffcd
--- /dev/null
+++ b/plinth/modules/pagekite/templatetags/pagekite_extras.py
@@ -0,0 +1,44 @@
+#
+# 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 django import template
+from plinth.modules.pagekite.util import prepare_service_for_display
+
+register = template.Library()
+
+
+@register.assignment_tag
+def create_pagekite_service_url(service, kite_name):
+ """Create a URL out of a pagekite service
+
+ Parameters: - service: the service params dictionary
+ - kite_name: kite name from the pagekite configuration, not
+ from the service params
+ """
+ # add extra information if it's missing
+ if 'subdomains' not in service:
+ service = prepare_service_for_display(service)
+
+ urlparams = {'protocol': service['protocol']}
+ if service['subdomains']:
+ urlparams['kite_name'] = "*.%s" % kite_name
+ else:
+ urlparams['kite_name'] = kite_name
+ url = "{protocol}://{kite_name}".format(**urlparams)
+ if 'frontend_port' in service and service['frontend_port']:
+ url = "%s:%s" % (url, service['frontend_port'])
+ return url
diff --git a/plinth/modules/pagekite/urls.py b/plinth/modules/pagekite/urls.py
index 7fa6c9918..32c13548d 100644
--- a/plinth/modules/pagekite/urls.py
+++ b/plinth/modules/pagekite/urls.py
@@ -20,10 +20,23 @@ URLs for the PageKite module
"""
from django.conf.urls import patterns, url
+from django.contrib.auth.decorators import login_required
+
+from .views import StandardServiceView, CustomServiceView, ConfigurationView, \
+ DeleteServiceView, index
urlpatterns = patterns( # pylint: disable-msg=C0103
- 'plinth.modules.pagekite.pagekite',
- url(r'^apps/pagekite/$', 'index', name='index'),
- url(r'^apps/pagekite/configure/$', 'configure', name='configure'),
+ 'plinth.modules.pagekite.views',
+ url(r'^apps/pagekite/$', login_required(index), name='index'),
+ url(r'^apps/pagekite/configure/$',
+ login_required(ConfigurationView.as_view()), name='configure'),
+ url(r'^apps/pagekite/services/standard$',
+ login_required(StandardServiceView.as_view()),
+ name='standard-services'),
+ url(r'^apps/pagekite/services/custom$',
+ login_required(CustomServiceView.as_view()), name='custom-services'),
+ url(r'^apps/pagekite/services/custom/delete$',
+ login_required(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..9dd01c834
--- /dev/null
+++ b/plinth/modules/pagekite/util.py
@@ -0,0 +1,147 @@
+#
+# 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 json
+import logging
+
+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.
+BACKEND_HOST = 'localhost'
+KITE_NAME = '@kitename'
+KITE_SECRET = '@kitesecret'
+
+SERVICE_PARAMS = ['protocol', 'kitename', 'backend_host', 'backend_port',
+ 'secret']
+
+# Predefined services are used to build the PredefinedServiceForm
+#
+# ATTENTION: When changing the params, make sure that the AddCustomServiceForm
+# still recognizes when you try to add a service equal to a predefined one
+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 get_pagekite_config():
+ """
+ Return the current PageKite configuration by executing various actions.
+ """
+ status = {}
+
+ # PageKite service enabled/disabled
+ # This assumes that if pagekite is running it's also enabled as a service
+ output = _run(['is-running'])
+ status['enabled'] = (output.split()[0] == 'yes')
+
+ # PageKite kite details
+ status.update(get_kite_details())
+
+ # PageKite frontend server
+ server = _run(['get-frontend'])
+ status['server'] = server.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('\n'):
+ if not serviceline: # skip empty lines
+ continue
+
+ service = json.loads(serviceline)
+ for name, predefined_service in PREDEFINED_SERVICES.items():
+ if service == predefined_service['params']:
+ predefined[name] = True
+ break
+ else:
+ custom.append(service)
+ return predefined, custom
+
+
+def prepare_service_for_display(service):
+ """ Add extra information that is used when displaying a service
+
+ - protocol is split into 'protocol' and 'frontend_port'
+ - detect whether 'subdomains' are supported (as boolean)
+ """
+ protocol = service['protocol']
+ if '/' in protocol:
+ service['protocol'], service['frontend_port'] = protocol.split('/')
+ service['subdomains'] = service['kitename'].startswith('*.')
+ return service
+
+
+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..185ed391d
--- /dev/null
+++ b/plinth/modules/pagekite/views.py
@@ -0,0 +1,133 @@
+#
+# 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.core.urlresolvers import reverse, reverse_lazy
+from django.http.response import HttpResponseRedirect
+from django.template.response import TemplateResponse
+from django.utils.decorators import method_decorator
+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, prepare_service_for_display
+from .forms import ConfigurationForm, StandardServiceForm, \
+ AddCustomServiceForm, DeleteCustomServiceForm
+
+
+required_packages = ('pagekite', 'augeas-tools', 'python-augeas')
+subsubmenu = [{'url': reverse_lazy('pagekite:index'),
+ 'text': _('About PageKite')},
+ {'url': reverse_lazy('pagekite:configure'),
+ 'text': _('Configure PageKite')},
+ {'url': reverse_lazy('pagekite:standard-services'),
+ 'text': _('Standard Services')},
+ {'url': reverse_lazy('pagekite:custom-services'),
+ 'text': _('Custom Services')}]
+
+
+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.
+
+ Also adds the requirement of all necessary packages to be installed
+ """
+ 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
+
+ @method_decorator(package.required(required_packages))
+ def dispatch(self, *args, **kwargs):
+ return super(ContextMixin, self).dispatch(*args, **kwargs)
+
+
+class DeleteServiceView(ContextMixin, View):
+ def post(self, request):
+ form = DeleteCustomServiceForm(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'] = AddCustomServiceForm(initial=service)
+ context['custom_services'] = [prepare_service_for_display(service)
+ for service in custom_services]
+ context.update(get_kite_details())
+ return context
+
+ def get(self, request, *args, **kwargs):
+ context = self.get_context_data(**kwargs)
+ form = AddCustomServiceForm(prefix="custom")
+ context['form'] = form
+ return self.render_to_response(context)
+
+ def post(self, request):
+ form = AddCustomServiceForm(request.POST, prefix="custom")
+ if form.is_valid():
+ form.save(request)
+ form = AddCustomServiceForm(prefix="custom")
+
+ context = self.get_context_data()
+ context['form'] = form
+
+ return self.render_to_response(context)
+
+
+class StandardServiceView(ContextMixin, FormView):
+ template_name = 'pagekite_standard_services.html'
+ title = 'PageKite Standard Services'
+ form_class = StandardServiceForm
+ success_url = reverse_lazy('pagekite:standard-services')
+
+ def get_initial(self):
+ return get_pagekite_services()[0]
+
+ def form_valid(self, form):
+ form.save(self.request)
+ return super(StandardServiceView, 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/tests/test_pagekite.py b/plinth/tests/test_pagekite.py
new file mode 100644
index 000000000..7463f25f5
--- /dev/null
+++ b/plinth/tests/test_pagekite.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python3
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+import unittest
+
+
+class TestPagekiteActions(unittest.TestCase):
+ """Test-cases for the pagekite action utils"""
+ _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'}
+ },
+ ]
+
+ @unittest.skip('Use this test once the function is in the pagekite module '
+ 'instead of actions/pagekite_util.py')
+ def test_convert_service_to_string(self):
+ """ Test deconstructing parameter dictionaries into strings """
+ for test in self._tests:
+ service_string = convert_service_to_string(test['params'])
+ self.assertEqual(test['line'], service_string)
diff --git a/setup.py b/setup.py
index 537b438f4..f71dcbc81 100755
--- a/setup.py
+++ b/setup.py
@@ -140,6 +140,10 @@ setuptools.setup(
glob.glob(os.path.join('actions', '*'))),
('/usr/share/man/man1', ['doc/plinth.1']),
('/etc/plinth', ['data/etc/plinth/plinth.config']),
+ ('/usr/share/augeas/lenses',
+ ['data/usr/share/augeas/lenses/pagekite.aug']),
+ ('/usr/share/augeas/lenses/tests',
+ ['data/usr/share/augeas/lenses/tests/test_pagekite.aug']),
('/etc/plinth/modules-enabled',
glob.glob(os.path.join('data/etc/plinth/modules-enabled',
'*')))],