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 %} + +
+ {% csrf_token %} + + + {{ form|bootstrap }} + + + +
+ +{% 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') +)