Merge remote-tracking branch 'fonfon/pagekite-augeas-new'

This commit is contained in:
James Valleroy 2015-05-16 10:35:58 -04:00
commit 44b045fef7
21 changed files with 1402 additions and 524 deletions

257
actions/pagekite Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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()

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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()

113
actions/pagekite_util.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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()

View File

@ -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(_):

41
actions/util.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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

View File

@ -0,0 +1,79 @@
(*
Module: Pagekite
Parses /etc/pagekite.d/
Author: Michael Pimmer <blubb@fonfon.at>
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

View File

@ -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" }

View File

View File

@ -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)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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 \
<a href="http://mybox1-myacc.pagekite.me">http://mybox1-myacc.pagekite.me \
</a>'))
ssh_enabled = forms.BooleanField(
label=_('Secure Shell (SSH)'), required=False,
help_text=_('See SSH client setup <a href="\
https://pagekite.net/wiki/Howto/SshOverPageKite/">instructions</a>'))
@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)

View File

@ -30,17 +30,12 @@
<div id='pagekite-post-enabled-form'
style='display: {{ form.enabled.value|yesno:'block,none' }};'>
<h3>PageKite Account</h3>
{% 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 }}
<h3>Services</h3>
{% include 'bootstrapform/field.html' with field=form.http_enabled %}
{% include 'bootstrapform/field.html' with field=form.ssh_enabled %}
</div>
<input type="submit" class="btn btn-primary" value="Update setup"/>
<input type="submit" class="btn btn-primary" value="Save settings"/>
</form>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load pagekite_extras %}
{% block page_head %}
<style type="text/css">
div.custom-services span.service {
display: inline-block;
padding-top: 6px;
}
form.pull-right button {
margin: 10px 5px;
}
.add-service input.btn {
margin: 10px 0px;
}
</style>
{% endblock %}
{% block content %}
<div class="alert alert-warning" role="alert">
<b>Warning:</b><br>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.
</div>
<div class="row custom-services">
<div class="col-lg-6">
<form class="form add-service" method="post">
<h4>Create a custom service</h4>
{{ form|bootstrap_horizontal:'col-lg-6' }}
{% csrf_token %}
<div class="form-group">
<div class=" col-lg-offset-6 col-lg-6">
<input type="submit" class="btn btn-primary" value="Add Service"/>
</div>
</div>
</form>
</div>
<div class="col-lg-5 col-lg-offset-1">
<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_url service kite_name as service_url %}
<div class="list-group-item clearfix">
<span class="service">
<span title="Connects {{ service_url }} to {{ service.backend_host }}:{{ service.backend_port }}">
{% if service_url|slice:":4" == "http" %}
<a href="{{ service_url }}">{{ service_url }}</a>
{% else %}
{{ service_url }}
{% endif %}
<br>
connected to {{ service.backend_host }}:{{ service.backend_port }}
</span>
</span>
<form class="form pull-right" method="post"
action="{% url 'pagekite:delete-custom-service' %}">
<div style='display:none'>
{% csrf_token %}
{{ service.form.as_p }}
</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>
{% endblock %}

View File

@ -22,7 +22,7 @@
<p>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: </p>
<ul>
@ -41,11 +41,10 @@ rest of the Internet. This includes the following situations: </p>
</ul>
<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
using a combination of tunnels and reverse proxies. You can use any
pagekite service provider, 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>

View File

@ -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 <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;
}
input.btn {
margin: 10px 15px;
}
</style>
{% endblock %}
{% block content %}
<div class="alert alert-warning" role="alert">
<b>Warning:</b><br>
<p>Published services are accessible and attackable from the evil internet.<p>
<p>Exposing SSH with the default password for 'fbx' is a VERY BAD idea.</p>
</div>
<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 Services"/>
</form>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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'),
)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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 "
"<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 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)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View File

@ -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',
'*')))],