diff --git a/actions/dynamicdns b/actions/dynamicdns
new file mode 100755
index 000000000..c8e6d673d
--- /dev/null
+++ b/actions/dynamicdns
@@ -0,0 +1,443 @@
+#!/bin/bash
+############################################################################
+# #
+# 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 . #
+# #
+# #
+# This script is a wrapper around ez-ipupdate and/or wget #
+# to update a Dynamic DNS account. The script is used as an #
+# interface between plinth and ez-ipupdate #
+# the script will store configuration, return configuration #
+# to plinth UI and do a dynamic DNS update. The script will #
+# also determe if we are behind a NAT device, if we can use #
+# ez-ipupdate tool or if we need to do some wget magic #
+# #
+# Todo: IPv6 #
+# Todo: GET WAN IP from Router via UPnP if supported #
+# Todo: licence string? #
+# author: Daniel Steglich #
+# #
+############################################################################
+
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+
+# static values
+WGET=$(which wget)
+WGETOPTIONS="-4 -o /dev/null -t 3 -T 3"
+EMPTYSTRING="none"
+NOIP="0.0.0.0"
+# how often do we poll for IP changes if we are behind a NAT?
+UPDATEMINUTES=5
+# if we do not have a IP check URL, how often should we do a "blind" update
+UPDATEMINUTESUNKNOWN=3600
+TOOLNAME=ez-ipupdate
+UPDATE_TOOL=$(which ${TOOLNAME})
+DISABLED_STRING='disabled'
+ENABLED_STRING='enabled'
+
+# Dirs and filenames
+CFGDIR="/etc/${TOOLNAME}/"
+CFG="${CFGDIR}${TOOLNAME}.conf"
+CFG_disabled="${CFGDIR}${TOOLNAME}.inactive"
+IPFILE="${CFGDIR}${TOOLNAME}.currentIP"
+STATUSFILE="${CFGDIR}${TOOLNAME}.status"
+LASTUPDATE="${CFGDIR}/last-update"
+HELPERCFG="${CFGDIR}${TOOLNAME}-plinth.cfg"
+CRONJOB="/etc/cron.d/${TOOLNAME}"
+PIDFILE="/var/run/ez-ipupdate.pid"
+
+# this function will parse commandline options
+doGetOpt()
+{
+ basicauth=0
+ ignoreCertError=0
+
+ while getopts ":s:d:u:p:I:U:c:b:" opt; do
+ case ${opt} in
+ s)
+ if [ "${OPTARG}" != "${EMPTYSTRING}" ];then
+ server=${OPTARG}
+ else
+ server=""
+ fi
+ ;;
+ d)
+ host=${OPTARG}
+ ;;
+ u)
+ user=${OPTARG}
+ ;;
+ p)
+ pass=${OPTARG}
+ ;;
+ I)
+ if [ "${OPTARG}" != "${EMPTYSTRING}" ];then
+ ipurl=${OPTARG}
+ else
+ ipurl=""
+ fi
+ ;;
+ U)
+ if [ "${OPTARG}" != "${EMPTYSTRING}" ];then
+ updateurl=${OPTARG}
+ else
+ updateurl=""
+ fi
+ ;;
+ b)
+ basicauth=${OPTARG}
+ ;;
+ c)
+ ignoreCertError=${OPTARG}
+ ;;
+ \?)
+ echo "Invalid option: -${OPTARG}" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+# this function will write a persistent config file to disk
+doWriteCFG()
+{
+ mkdir ${CFGDIR} 2> /dev/null
+ # always write to the inactive config - needs to be enabled via "start" command later
+ local out_file=${CFG_disabled}
+
+ # reset the last update time
+ echo 0 > ${LASTUPDATE}
+
+ # reset the last updated IP
+ echo "0.0.0.0" > ${IPFILE}
+
+ # reset last update (if there is one)
+ rm ${STATUSFILE} 2> /dev/null
+
+ # find the interface (always the default gateway interface)
+ default_interface=$(ip route |grep default |awk '{print $5}')
+
+ # store the given options in ez-ipupdate compatible config file
+ {
+ echo "host=${host}"
+ echo "server=${server}"
+ echo "user=${user}:${pass}"
+ echo "service-type=gnudip"
+ echo "retrys=3"
+ echo "wildcard"
+ } > ${out_file}
+
+ # store UPDATE URL params
+ {
+ echo "POSTURL ${updateurl}"
+ echo "POSTAUTH ${basicauth}"
+ echo "POSTSSLIGNORE ${ignoreCertError}"
+ } > ${HELPERCFG}
+
+ # check if we are behind a NAT Router
+ echo "IPURL ${ipurl}" >> ${HELPERCFG}
+ if [ -z ${ipurl} ];then
+ echo "NAT unknown" >> ${HELPERCFG}
+ else
+ doGetWANIP
+ ISGLOBAL=$(ip addr ls "${default_interface}" | grep "${wanip}")
+ if [ -z "${ISGLOBAL}" ];then
+ # we are behind NAT
+ echo "NAT yes" >> ${HELPERCFG}
+ else
+ # we are directly connected
+ echo "NAT no" >> ${HELPERCFG}
+ # if this file is added ez-ipupdate will take ip form this interface
+ {
+ "interface=${default_interface}"
+ # if this line is added to config file, ez-ipupdate will be launched on startup via init.d
+ "daemon"
+ "execute=${0} success"
+ } > ${out_file}
+ fi
+ fi
+}
+
+# this function will read the config file from disk
+# special treatment for empty strings is done here:
+# plinth will give empty strings like: ''
+# but we don't want this single quotes to be used
+doReadCFG()
+{
+ host=""
+ server=""
+ user=""
+ pass=""
+ ipurl=""
+
+ if [ ! -z "${cfgfile}" ];then
+ host=$(grep host "${cfgfile}" 2> /dev/null |cut -d = -f 2)
+ server=$(grep server "${cfgfile}" 2> /dev/null |cut -d = -f 2 |grep -v ^\'\')
+ user=$(grep user "${cfgfile}" 2> /dev/null |cut -d = -f 2 |cut -d : -f 1 )
+ pass=$(grep user "${cfgfile}" 2> /dev/null |cut -d = -f 2 |cut -d : -f 2)
+ fi
+
+ if [ ! -z ${HELPERCFG} ];then
+ ipurl=$(grep ^IPURL "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\')
+ updateurl=$(grep POSTURL "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\')
+ basicauth=$(grep POSTAUTH "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\')
+ ignoreCertError=$(grep POSTSSLIGNORE "${HELPERCFG}" 2> /dev/null |awk '{print $2}' |grep -v ^\'\')
+ fi
+}
+
+# replace vars from url: i.e.:
+# https://example.com/update.php?domain=&User=&Pass=
+# also this function will remove the surounding single quotes from the URL string
+# as plinth will add them
+doReplaceVars()
+{
+ local url=$(echo "${updateurl}" | sed "s//${wanip}/g")
+ url=$(echo "${url}" | sed "s//${host}/g")
+ url=$(echo "${url}" | sed "s//${user}/g")
+ url=$(echo "${url}" | sed "s//${pass}/g")
+ url=$(echo "${url}" | sed "s/'//g")
+ updateurl=${url}
+ logger "expanded update URL as ${url}"
+}
+
+# doReadCFG() needs to be run before this
+# this function will return all configured parameters in a way that
+# plinth will understand (plinth know the order of
+# parameters this function will return)
+doStatus()
+{
+ PROC=$(pgrep ${TOOLNAME})
+ if [ -f "${CRONJOB}" ];then
+ echo "${ENABLED_STRING}"
+ elif [ ! -z "${PROC}" ];then
+ echo "${ENABLED_STRING}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${server}" ];then
+ echo "${server}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${host}" ];then
+ echo "${host}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${user}" ];then
+ echo "${user}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${pass}" ];then
+ echo "${pass}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${ipurl}" ];then
+ echo "${ipurl}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${updateurl}" ];then
+ echo "${updateurl}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${basicauth}" ];then
+ echo "${basicauth}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+ if [ ! -z "${ignoreCertError}" ];then
+ echo "${ignoreCertError}"
+ else
+ echo "${DISABLED_STRING}"
+ fi
+}
+
+# ask a public WEB Server for the WAN IP we are comming from
+# and store this ip within $wanip
+doGetWANIP()
+{
+ if [ ! -z "${ipurl}" ];then
+ outfile=$(mktemp)
+ local cmd="${WGET} ${WGETOPTIONS} -O ${outfile} ${ipurl}"
+ $cmd
+ wanip=$(sed s/[^0-9.]//g "${outfile}")
+ rm "${outfile}"
+ [ -z "${wanip}" ] && wanip=${NOIP}
+ else
+ # no WAN IP found because of missing check URL
+ wanip=${NOIP}
+ fi
+}
+
+# actualy do the update (using wget or ez-ipupdate or even both)
+# this function is called via cronjob
+doUpdate()
+{
+ local dnsentry=$(nslookup "${host}"|tail -n2|grep A|sed s/[^0-9.]//g)
+ if [ "${dnsentry}" = "${wanip}" ];then
+ return
+ fi
+ if [ ! -z "${server}" ];then
+ start-stop-daemon -S -x "${UPDATE_TOOL}" -m -p "${PIDFILE}" -- -c "${cfgfile}"
+ fi
+ if [ ! -z "${updateurl}" ];then
+ doReplaceVars
+ if [ "${basicauth}" = "enabled" ];then
+ WGETOPTIONS="${WGETOPTIONS} --user ${user} --password ${pass} "
+ fi
+ if [ "${ignoreCertError}" = "enabled" ];then
+ WGETOPTIONS="${WGETOPTIONS} --no-check-certificate "
+ fi
+ local cmd="${WGET} ${WGETOPTIONS} ${updateurl}"
+ $cmd
+ # ToDo: check the returning text from WEB Server. User need to give expected string.
+ if [ ${?} -eq 0 ];then
+ ${0} success ${wanip}
+ else
+ ${0} failed
+ fi
+ fi
+}
+
+cmd=${1}
+shift
+# decide which config to use
+cfgfile="/tmp/none"
+[ -f ${CFG_disabled} ] && cfgfile=${CFG_disabled}
+[ -f ${CFG} ] && cfgfile=${CFG}
+
+# check what action is requested
+case ${cmd} in
+ configure)
+ doGetOpt "${@}"
+ doWriteCFG
+ ;;
+ start)
+ doGetWANIP
+ if [ "$(grep ^NAT ${HELPERCFG} | awk '{print $2}')" = "no" ];then
+ #if we are not behind a NAT device and we use gnudip, start the daemon tool
+ local gnudipServer=$(grep server ${cfgfile} 2> /dev/null |cut -d = -f 2 |grep -v ^\'\')
+ if [ -f ${CFG} -a ! -z "${gnudipServer}" ];then
+ mv ${CFG_disabled} ${CFG}
+ /etc/init.d/${TOOLNAME} start
+ fi
+ # if we are not behind a NAT device and we use update-URL, add a cronjob
+ # (daemon tool does not support update-URL feature)
+ if [ ! -z "$(grep ^POSTURL $HELPERCFG | awk '{print $2}')" ];then
+ echo "*/${UPDATEMINUTES} * * * * root ${0} update" > ${CRONJOB}
+ $0 update
+ fi
+ else
+ # if we are behind a NAT device, add a cronjob (daemon tool cannot monitor WAN IP changes)
+ echo "*/${UPDATEMINUTES} * * * * root ${0} update" > $CRONJOB
+ $0 update
+ fi
+ ;;
+ get-nat)
+ NAT=$(grep ^NAT $HELPERCFG 2> /dev/null | awk '{print $2}')
+ [ -z "${NAT}" ] && NAT="unknown"
+ echo ${NAT}
+ ;;
+ update)
+ doReadCFG
+ dnsentry=$(nslookup "${host}"|tail -n2|grep A|sed s/[^0-9.]//g)
+ doGetWANIP
+ echo ${wanip} > ${IPFILE}
+ grep -v execute ${cfgfile} > ${cfgfile}.tmp
+ mv ${cfgfile}.tmp ${cfgfile}
+ echo "execute=${0} success ${wanip}" >> ${cfgfile}
+ # if we know our WAN IP, only update if IP changes
+ if [ "${dnsentry}" != "${wanip}" -a "${wanip}" != ${NOIP} ];then
+ doUpdate
+ fi
+ # if we don't know our WAN IP do a blind update once a hour
+ if [ "${wanip}" = ${NOIP} ];then
+ uptime=$(cut -d . -f 1 /proc/uptime)
+ LAST=0
+ [ -f ${LASTUPDATE} ] && LAST=$(cat ${LASTUPDATE})
+ diff=$((uptime - LAST))
+ if [ ${diff} -gt ${UPDATEMINUTESUNKNOWN} ];then
+ doUpdate
+ fi
+ fi
+ ;;
+ stop)
+ rm ${CRONJOB} 2> /dev/null
+ /etc/init.d/${TOOLNAME} stop
+ kill "$(cat ${PIDFILE})" 2> /dev/null
+ mv ${CFG} ${CFG_disabled}
+ ;;
+ success)
+ date=$(date)
+ echo "last update done (${date})" > ${STATUSFILE}
+ awk '{print $1}' /proc/uptime |cut -d . -f 1 > ${LASTUPDATE}
+ # if called from cronjob, the current IP is given as parameter
+ if [ $# -eq 1 ];then
+ echo "${1}" > ${IPFILE}
+ else
+ # if called from ez-ipupdate daemon, no WAN IP is given as parameter
+ doGetWANIP
+ echo ${wanip} > ${IPFILE}
+ fi
+ ;;
+ failed)
+ date=$(date)
+ echo "last update failed (${date})" > ${STATUSFILE}
+ ;;
+ get-last-success)
+ if [ -f ${STATUSFILE} ];then
+ cat ${STATUSFILE}
+ else
+ echo "no successful update recorded since last config change"
+ fi
+ ;;
+ status)
+ doReadCFG
+ doStatus
+ ;;
+ get-timer)
+ echo ${UPDATEMINUTES}
+ ;;
+ clean)
+ rm ${CFGDIR}/*
+ rm ${CRONJOB}
+ ;;
+ *)
+ echo "usage: status|configure |start|stop|update|get-nat|clean|success [updated IP]|failed|get-last-success"
+ echo ""
+ echo "options are:"
+ echo "-s Gnudip Server address"
+ echo "-d Domain to be updated"
+ echo "-u Account username"
+ echo "-p Account Password"
+ echo "-I A URL which returns the IP of the client who is requesting"
+ echo "-U The update URL (a HTTP GET on this URL will be done)"
+ echo "-c <1|0> disable SSL check on Update URL"
+ echo "-b <1|0> use HTTP basic auth on Update URL"
+ echo ""
+ echo "update do a one time update"
+ echo "clean delete configuration"
+ echo "success store update success and optional the updated IP"
+ echo "failed store update failure"
+ echo "get-nat return the detected nat type"
+ echo "get-last-success return date of last successful update"
+ ;;
+esac
+exit 0
diff --git a/data/etc/plinth/modules-enabled/dynamicdns b/data/etc/plinth/modules-enabled/dynamicdns
new file mode 100644
index 000000000..af54a38dd
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/dynamicdns
@@ -0,0 +1 @@
+plinth.modules.dynamicdns
diff --git a/plinth/modules/dynamicdns/__init__.py b/plinth/modules/dynamicdns/__init__.py
new file mode 100644
index 000000000..747482e9d
--- /dev/null
+++ b/plinth/modules/dynamicdns/__init__.py
@@ -0,0 +1,27 @@
+#
+# 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 to configure ez-ipupdate client
+"""
+
+from . import dynamicdns
+from .dynamicdns import init
+
+__all__ = ['dynamicdns', 'init']
+
+depends = ['plinth.modules.apps']
diff --git a/plinth/modules/dynamicdns/dynamicdns.py b/plinth/modules/dynamicdns/dynamicdns.py
new file mode 100644
index 000000000..c57d1c4d9
--- /dev/null
+++ b/plinth/modules/dynamicdns/dynamicdns.py
@@ -0,0 +1,392 @@
+#
+# 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 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__)
+EMPTYSTRING = 'none'
+
+subsubmenu = [{'url': reverse_lazy('dynamicdns:index'),
+ 'text': _('About')},
+ {'url': reverse_lazy('dynamicdns:configure'),
+ 'text': _('Configure')},
+ {'url': reverse_lazy('dynamicdns:statuspage'),
+ 'text': _('Status')}
+ ]
+
+
+def init():
+ """Initialize the dynamicdns module"""
+ menu = cfg.main_menu.get('apps:index')
+ menu.add_urlname('Dynamic DNS', 'glyphicon-refresh',
+ 'dynamicdns:index', 40)
+
+
+@login_required
+@package.required('ez-ipupdate')
+def index(request):
+ """Serve dynamic DNS page"""
+
+ return TemplateResponse(request, 'dynamicdns.html',
+ {'title': _('dynamicdns'),
+ '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):
+ """Form to configure the dynamic DNS client"""
+
+ hlp_updt_url = 'The Variables <User>, <Pass>, <Ip>, \
+ <Domain> may be used within the URL. For details\
+ see the update URL templates of the example providers.'
+
+ hlp_services = 'Please choose an update protocol according to your \
+ provider. If your provider does not support the GnudIP \
+ protocol or your provider is not listed you may use \
+ the update URL of your provider.'
+
+ hlp_server = 'Please do not enter a URL here (like "https://example.com/")\
+ but only the hostname of the GnuDIP server (like \
+ "example.com").'
+
+ hlp_domain = 'The public domain name you want use to reach your box.'
+
+ hlp_disable_ssl = 'Use this option if your provider uses self signed \
+ certificates.'
+
+ hlp_http_auth = 'If this option is selected, your username and \
+ password will be used for HTTP basic authentication.'
+
+ hlp_secret = 'Leave this field empty \
+ if you want to keep your previous configured password.'
+
+ hlp_ipurl = 'Optional Value. If your FreedomBox is not connected \
+ directly to the Internet (i.e. connected to a NAT \
+ router) this URL is used to figure out the real Internet \
+ IP. The URL should simply return the IP where the \
+ client comes from. Example: \
+ http://myip.datasystems24.de'
+
+ hlp_user = 'You should have been requested to select a username \
+ when you created the account.'
+
+ """ToDo: sync this list with the html template file"""
+ provider_choices = (
+ ('GnuDIP', 'GnuDIP'),
+ ('noip', 'noip.com'),
+ ('selfhost', 'selfhost.bz'),
+ ('freedns', 'freedns.afraid.org'),
+ ('other', 'other update URL'))
+
+ enabled = forms.BooleanField(label=_('Enable Dynamic DNS'),
+ required=False)
+
+ service_type = forms.ChoiceField(label=_('Service type'),
+ help_text=_(hlp_services),
+ choices=provider_choices)
+
+ dynamicdns_server = TrimmedCharField(
+ label=_('GnudIP Server Address'),
+ required=False,
+ help_text=_(hlp_server),
+ validators=[
+ validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$',
+ _('Invalid server name'))])
+
+ dynamicdns_update_url = TrimmedCharField(label=_('Update URL'),
+ required=False,
+ help_text=_(hlp_updt_url))
+
+ disable_SSL_cert_check = forms.BooleanField(label=_('accept all SSL \
+ certificates'),
+ help_text=_(hlp_disable_ssl),
+ required=False)
+
+ use_http_basic_auth = forms.BooleanField(label=_('use HTTP basic \
+ authentication'),
+ help_text=_(hlp_http_auth),
+ required=False)
+
+ dynamicdns_domain = TrimmedCharField(
+ label=_('Domain Name'),
+ help_text=_(hlp_domain),
+ required=False,
+ validators=[
+ validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$',
+ _('Invalid domain name'))])
+
+ dynamicdns_user = TrimmedCharField(
+ label=_('Username'),
+ required=False,
+ help_text=_(hlp_user))
+
+ dynamicdns_secret = TrimmedCharField(
+ label=_('Password'), widget=forms.PasswordInput(),
+ required=False,
+ help_text=_(hlp_secret))
+
+ showpw = forms.BooleanField(label=_('show password'),
+ required=False)
+
+ dynamicdns_ipurl = TrimmedCharField(
+ label=_('IP check URL'),
+ required=False,
+ help_text=_(hlp_ipurl),
+ validators=[
+ validators.URLValidator(schemes=['http', 'https', 'ftp'])])
+
+ def clean(self):
+ cleaned_data = super(ConfigureForm, self).clean()
+ dynamicdns_secret = cleaned_data.get('dynamicdns_secret')
+ dynamicdns_update_url = cleaned_data.get('dynamicdns_update_url')
+ dynamicdns_user = cleaned_data.get('dynamicdns_user')
+ dynamicdns_domain = cleaned_data.get('dynamicdns_domain')
+ dynamicdns_server = cleaned_data.get('dynamicdns_server')
+ service_type = cleaned_data.get('service_type')
+ old_dynamicdns_secret = self.initial['dynamicdns_secret']
+
+ """clear the fields which are not in use"""
+ if service_type == 'GnuDIP':
+ dynamicdns_update_url = ""
+ else:
+ dynamicdns_server = ""
+
+ if cleaned_data.get('enabled'):
+ """check if gnudip server or update URL is filled"""
+ if not dynamicdns_update_url and not dynamicdns_server:
+ raise forms.ValidationError('please give update URL or \
+ a GnuDIP Server')
+ LOGGER.info('no server address given')
+
+ if dynamicdns_server and not dynamicdns_user:
+ raise forms.ValidationError('please give GnuDIP username')
+
+ if dynamicdns_server and not dynamicdns_domain:
+ raise forms.ValidationError('please give GnuDIP domain')
+
+ """check if a password was set before or a password is set now"""
+ if (dynamicdns_server and not dynamicdns_secret
+ and not old_dynamicdns_secret):
+ raise forms.ValidationError('please give a password')
+ LOGGER.info('no password given')
+
+
+@login_required
+@package.required('ez-ipupdate')
+def configure(request):
+ """Serve the configuration form"""
+ status = get_status()
+ form = None
+
+ if request.method == 'POST':
+ form = ConfigureForm(request.POST, initial=status)
+ if form.is_valid():
+ _apply_changes(request, status, form.cleaned_data)
+ status = get_status()
+ form = ConfigureForm(initial=status)
+ else:
+ form = ConfigureForm(initial=status)
+
+ return TemplateResponse(request, 'dynamicdns_configure.html',
+ {'title': _('Configure dynamicdns Client'),
+ 'form': form,
+ 'subsubmenu': subsubmenu})
+
+
+@login_required
+@package.required('ez-ipupdate')
+def statuspage(request):
+ """Serve the status page """
+ check_nat = actions.run('dynamicdns', ['get-nat'])
+ last_update = actions.run('dynamicdns', ['get-last-success'])
+
+ no_nat = check_nat.strip() == 'no'
+ nat_unchecked = check_nat.strip() == 'unknown'
+ timer = actions.run('dynamicdns', ['get-timer'])
+
+ if no_nat:
+ LOGGER.info('we are not behind a NAT')
+
+ if nat_unchecked:
+ LOGGER.info('we did not checked if we are behind a NAT')
+
+ return TemplateResponse(request, 'dynamicdns_status.html',
+ {'title': _('Status of dynamicdns Client'),
+ 'no_nat': no_nat,
+ 'nat_unchecked': nat_unchecked,
+ 'timer': timer,
+ 'last_update': last_update,
+ 'subsubmenu': subsubmenu})
+
+
+def get_status():
+ """Return the current status"""
+ """ToDo: use key/value instead of hard coded value list"""
+ status = {}
+ output = actions.run('dynamicdns', 'status')
+ details = output.split()
+ status['enabled'] = (output.split()[0] == 'enabled')
+
+ if len(details) > 1:
+ if details[1] == 'disabled':
+ status['dynamicdns_server'] = ''
+ else:
+ status['dynamicdns_server'] = details[1].replace("'", "")
+ else:
+ status['dynamicdns_server'] = ''
+
+ if len(details) > 2:
+ if details[2] == 'disabled':
+ status['dynamicdns_domain'] = ''
+ else:
+ status['dynamicdns_domain'] = details[2]
+ status['dynamicdns_domain'] = details[2].replace("'", "")
+ else:
+ status['dynamicdns_domain'] = ''
+
+ if len(details) > 3:
+ if details[3] == 'disabled':
+ status['dynamicdns_user'] = ''
+ else:
+ status['dynamicdns_user'] = details[3].replace("'", "")
+ else:
+ status['dynamicdns_user'] = ''
+
+ if len(details) > 4:
+ if details[4] == 'disabled':
+ status['dynamicdns_secret'] = ''
+ else:
+ status['dynamicdns_secret'] = details[4].replace("'", "")
+ else:
+ status['dynamicdns_secret'] = ''
+
+ if len(details) > 5:
+ if details[5] == 'disabled':
+ status['dynamicdns_ipurl'] = ''
+ else:
+ status['dynamicdns_ipurl'] = details[5].replace("'", "")
+ else:
+ status['dynamicdns_ipurl'] = ''
+
+ if len(details) > 6:
+ if details[6] == 'disabled':
+ status['dynamicdns_update_url'] = ''
+ else:
+ status['dynamicdns_update_url'] = details[6].replace("'", "")
+ else:
+ status['dynamicdns_update_url'] = ''
+
+ if len(details) > 7:
+ status['disable_SSL_cert_check'] = (output.split()[7] == 'enabled')
+ else:
+ status['disable_SSL_cert_check'] = False
+
+ if len(details) > 8:
+ status['use_http_basic_auth'] = (output.split()[8] == 'enabled')
+ else:
+ status['use_http_basic_auth'] = False
+
+ if not status['dynamicdns_server'] and not status['dynamicdns_update_url']:
+ status['service_type'] = 'GnuDIP'
+ elif not status['dynamicdns_server'] and status['dynamicdns_update_url']:
+ status['service_type'] = 'other'
+ else:
+ status['service_type'] = 'GnuDIP'
+
+ return status
+
+
+def _apply_changes(request, old_status, new_status):
+ """Apply the changes to Dynamic DNS client"""
+ LOGGER.info('New status is - %s', new_status)
+ LOGGER.info('Old status was - %s', old_status)
+
+ if new_status['dynamicdns_secret'] == '':
+ new_status['dynamicdns_secret'] = old_status['dynamicdns_secret']
+
+ if new_status['dynamicdns_ipurl'] == '':
+ new_status['dynamicdns_ipurl'] = EMPTYSTRING
+
+ if new_status['dynamicdns_update_url'] == '':
+ new_status['dynamicdns_update_url'] = EMPTYSTRING
+
+ if new_status['dynamicdns_server'] == '':
+ new_status['dynamicdns_server'] = EMPTYSTRING
+
+ if new_status['service_type'] == 'GnuDIP':
+ new_status['dynamicdns_update_url'] = EMPTYSTRING
+ else:
+ new_status['dynamicdns_server'] = EMPTYSTRING
+
+ if old_status != new_status:
+ disable_ssl_check = "disabled"
+ use_http_basic_auth = "disabled"
+
+ if new_status['disable_SSL_cert_check']:
+ disable_ssl_check = "enabled"
+
+ if new_status['use_http_basic_auth']:
+ use_http_basic_auth = "enabled"
+
+ _run(['configure', '-s', new_status['dynamicdns_server'],
+ '-d', new_status['dynamicdns_domain'],
+ '-u', new_status['dynamicdns_user'],
+ '-p', new_status['dynamicdns_secret'],
+ '-I', new_status['dynamicdns_ipurl'],
+ '-U', new_status['dynamicdns_update_url'],
+ '-c', disable_ssl_check,
+ '-b', use_http_basic_auth])
+
+ if old_status['enabled']:
+ _run(['stop'])
+ if new_status['enabled']:
+ _run(['start'])
+
+ messages.success(request,
+ _('Dynamic DNS configuration is updated!'))
+ else:
+ LOGGER.info('nothing changed')
+
+
+def _run(arguments, superuser=False):
+ """Run a given command and raise exception if there was an error"""
+ command = 'dynamicdns'
+
+ if superuser:
+ return actions.superuser_run(command, arguments)
+ else:
+ return actions.run(command, arguments)
diff --git a/plinth/modules/dynamicdns/templates/dynamicdns.html b/plinth/modules/dynamicdns/templates/dynamicdns.html
new file mode 100644
index 000000000..3b3bac840
--- /dev/null
+++ b/plinth/modules/dynamicdns/templates/dynamicdns.html
@@ -0,0 +1,46 @@
+{% 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 %}
+
+{% block content %}
+DynamicDNS client
+If your internet provider changes your IP address periodic (i.e. every 24h)
+ it may be hard for others to find you in the WEB. And for this reason nobody
+ may find the services which are provided by FreedomBox (like your ownCloud).
+
+ The solution is to assign a DNS name to your IP address and update the DNS
+ name every time your IP is changed by your Internet provider. Dynamic DNS
+ allows you to push your current public IP address to an
+ gnudip
+ server. Afterwards the Server will assign your DNS name with the new IP
+ and if someone from the internet asks for your DNS name he will get your
+ current IP answered.
+
+ If you are looking for a free dynamic DNS account, you may find a free
+ GnuDIP service at gnudip.datasystems24.net or you may find free update
+ URL based services on
+
+ freedns.afraid.org
+
+ If your freedombox is connected behind some NAT router, don't forget
+ to add portforwarding (i.e. forward some standard ports like 80 and 443)
+ to your freedombox device.
+
+{% endblock %}
diff --git a/plinth/modules/dynamicdns/templates/dynamicdns_configure.html b/plinth/modules/dynamicdns/templates/dynamicdns_configure.html
new file mode 100644
index 000000000..38489dee5
--- /dev/null
+++ b/plinth/modules/dynamicdns/templates/dynamicdns_configure.html
@@ -0,0 +1,164 @@
+{% 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 content %}
+
+
+
+{% endblock %}
+
+{% block page_js %}
+
+{% endblock %}
diff --git a/plinth/modules/dynamicdns/templates/dynamicdns_status.html b/plinth/modules/dynamicdns/templates/dynamicdns_status.html
new file mode 100644
index 000000000..dad90326a
--- /dev/null
+++ b/plinth/modules/dynamicdns/templates/dynamicdns_status.html
@@ -0,0 +1,42 @@
+{% 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 %}
+
+{% block content %}
+
+
NAT type
+ {% if nat_unchecked %}
+ NAT type not detected yet, if you do not provide a "IP check URL" we will
+ not detect a NAT type.
+ {% else %}
+ {% if no_nat %}
+ Direct connection to the internet.
+ {% else %}
+ Behind NAT, this means that dynamic DNS service will poll the
+ "IP check URL" for changes (we need the "IP check URL" for this reason
+ - otherwise we will not detect IP changes).
+ It may take up to {{ timer }} minutes until we update your DNS entry in
+ case of WAN IP change.
+ {% endif %}
+ {% endif %}
+ Last update
+ {{ last_update }}
+
+
+{% endblock %}
diff --git a/plinth/modules/dynamicdns/urls.py b/plinth/modules/dynamicdns/urls.py
new file mode 100644
index 000000000..04ff51485
--- /dev/null
+++ b/plinth/modules/dynamicdns/urls.py
@@ -0,0 +1,30 @@
+#
+# 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 .
+#
+
+"""
+URLs for the dynamicdns module
+"""
+
+from django.conf.urls import patterns, url
+
+
+urlpatterns = patterns(
+ 'plinth.modules.dynamicdns.dynamicdns',
+ url(r'^apps/dynamicdns/$', 'index', name='index'),
+ url(r'^apps/dynamicdns/configure/$', 'configure', name='configure'),
+ url(r'^apps/dynamicdns/statuspage/$', 'statuspage', name='statuspage')
+)