# # 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 django import forms from django.contrib import messages from django.core import validators from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _, ugettext_lazy import json import logging import requests from . import utils from plinth import cfg from plinth.errors import ActionError, DomainRegistrationError from plinth.modules.pagekite.utils import PREDEFINED_SERVICES, run from plinth.utils import format_lazy 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 SubdomainWidget(forms.widgets.TextInput): """Append the domain to the subdomain bootstrap input field""" def __init__(self, domain, *args, **kwargs): """Intialize the widget by storing the domain value.""" super().__init__(*args, **kwargs) self.domain = domain def render(self, *args, **kwargs): """Return the HTML for the widget.""" inputfield = super().render(*args, **kwargs) return """
{0} {1}
""".format(inputfield, self.domain) class ConfigurationForm(forms.Form): """Configure PageKite credentials and frontend""" enabled = forms.BooleanField( label=ugettext_lazy('Enable PageKite'), required=False) server_domain = forms.CharField( label=ugettext_lazy('Server domain'), required=False, help_text=ugettext_lazy( 'Select your pagekite server. Set "pagekite.net" to use ' 'the default pagekite.net server.'), widget=forms.TextInput()) server_port = forms.IntegerField( label=ugettext_lazy('Server port'), required=False, help_text=ugettext_lazy('Port of your pagekite server (default: 80)')) kite_name = TrimmedCharField( label=ugettext_lazy('Kite name'), help_text=ugettext_lazy('Example: mybox.pagekite.me'), validators=[ validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$', ugettext_lazy('Invalid kite name'))]) kite_secret = TrimmedCharField( label=ugettext_lazy('Kite secret'), help_text=ugettext_lazy( '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): """Save the form on submission after validation.""" 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']: utils.run(['set-kite', '--kite-name', new['kite_name']], input=new['kite_secret'].encode()) messages.success(request, _('Kite details set')) config_changed = True if old['server_domain'] != new['server_domain'] or \ old['server_port'] != new['server_port']: server = "%s:%s" % (new['server_domain'], new['server_port']) utils.run(['set-frontend', server]) messages.success(request, _('Pagekite server set')) config_changed = True if old['enabled'] != new['enabled']: if new['enabled']: utils.run(['start-and-enable']) messages.success(request, _('PageKite enabled')) else: utils.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']: utils.run(['restart']) # Update kite name registered with Name Services module. utils.update_names_module(enabled=new['enabled'], kite_name=new['kite_name']) 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 = utils.get_kite_details() for name, service in utils.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 utils.PREDEFINED_SERVICES.keys(): if self.initial[service_name] != formdata[service_name]: service = utils.PREDEFINED_SERVICES[service_name]['params'] service = json.dumps(service) if formdata[service_name]: utils.run(['add-service', '--service', service]) messages.success(request, _('Service enabled: {name}') .format(name=service_name)) else: utils.run(['remove-service', '--service', service]) messages.success(request, _('Service disabled: {name}') .format(name=service_name)) # Update kite services registered with Name Services module. utils.update_names_module() 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=ugettext_lazy('protocol')) frontend_port = forms.IntegerField( min_value=0, max_value=65535, label=ugettext_lazy('external (frontend) port'), required=True) backend_port = forms.IntegerField( min_value=0, max_value=65535, label=ugettext_lazy('internal (freedombox) port')) subdomains = forms.BooleanField( label=ugettext_lazy('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" % utils.KITE_NAME else: formdata['kitename'] = utils.KITE_NAME if 'secret' not in formdata: formdata['secret'] = utils.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'] = utils.BACKEND_HOST return formdata class DeleteCustomServiceForm(BaseCustomServiceForm): """Form to remove custom service.""" def delete(self, request): service = self.convert_formdata_to_service(self.cleaned_data) utils.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 utils.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: utils.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 class FirstBootForm(forms.Form): """Set up freedombox.me pagekite subdomain""" DOMAIN_APPENDIX = '.freedombox.me' # Webservice url for domain validation and registration service_url = 'http://freedombox.me/cgi-bin/freedomkite.pl' code_help_text = format_lazy( ugettext_lazy('The voucher you received with your {box_name} Danube ' 'Edition'), box_name=ugettext_lazy(cfg.box_name)) code = forms.CharField(help_text=code_help_text) domain = forms.SlugField(label=_('Subdomain'), widget=SubdomainWidget(domain=DOMAIN_APPENDIX), help_text=_('The subdomain you want to register')) def clean_domain(self): """Append the domain to the users' subdomain""" return self.cleaned_data['domain'] + self.DOMAIN_APPENDIX def clean(self): """Validate user input (subdomain and code)""" cleaned_data = super().clean() # If the subdomain is wrong, don't look if the domain is # available if self.errors: return cleaned_data self.domain_already_registered = False code = cleaned_data.get('code') domain = cleaned_data.get('domain') response = requests.get(self.service_url, params={'code': code}).json() # 1. Code is invalid: {} if 'domain' not in response: raise ValidationError(_('This code is not valid'), code='invalid') # 2. Code is valid, domain registered: {'domain': 'xx.freedombox.me'} elif response['domain']: if response['domain'] == domain: self.domain_already_registered = True else: message = _('This code is bound to the domain {domain}.') \ .format(domain=response['domain']) raise ValidationError(message, code='invalid') # 3. Code is valid, no domain registered: {'domain': None} elif response['domain'] is None: # Make sure that the desired domain is available data = {'domain': domain} domain_response = requests.get(self.service_url, params=data) registered_domain = domain_response.json()['domain'] if registered_domain is not None: message = _('The requested domain is already registered.') raise ValidationError(message, code='invalid') return cleaned_data def register_domain(self): """Register a domain (only if it's not already registered)""" if self.domain_already_registered: return data = {'domain': self.cleaned_data['domain'], 'code': self.cleaned_data['code']} response = requests.post(self.service_url, data) if not response.ok: message = _('Domain registration failed: {response}.').format( response=response.text) LOGGER.error(message) raise DomainRegistrationError(message) def setup_pagekite(self): """Configure and enable PageKite service.""" # Set kite name and secret run(['set-kite', '--kite-name', self.cleaned_data['domain']], input=self.cleaned_data['code'].encode()) # Set frontend run(['set-frontend', '%s:80' % self.cleaned_data['domain']]) # Enable PageKite HTTP + HTTPS service for service_name in ['http', 'https']: service = PREDEFINED_SERVICES[service_name]['params'] try: run(['add-service', '--service', json.dumps(service)]) except ActionError as err: if 'already exists' not in str(err): raise run(['start-and-enable'])