added a module and a action script to manage ez-ipupdate tool, which provides dynamic IP updates to a gnudip dynamic DNS Server

This commit is contained in:
Daniel Steglich 2015-01-04 12:14:23 +00:00
parent 4010b81ecc
commit 56208e007d
8 changed files with 682 additions and 0 deletions

261
actions/dynamicDNS Executable file
View File

@ -0,0 +1,261 @@
#!/bin/bash
#Todo: IPv6
#Todo: Other service types than gnudip (generic update URL)
#Todo: GET WAN IP from Router via UPnP if supported
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
#static values
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})
#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}"
doReadCFG()
{
host=""
server=""
user=""
pass=""
IPURL=""
FILE=""
[ -f $CFG_disabled ] && FILE=$CFG_disabled
[ -f $CFG ] && FILE=$CFG
if [ ! -z $FILE ];then
host=`cat $FILE 2> /dev/null |grep host |cut -d = -f 2`
server=`cat $FILE 2> /dev/null |grep server |cut -d = -f 2`
user=`cat $FILE 2> /dev/null |grep user |cut -d = -f 2 |cut -d : -f 1`
pass=`cat $FILE 2> /dev/null |grep user |cut -d = -f 2 |cut -d : -f 2`
fi
if [ ! -z $HELPERCFG ];then
IPURL=`cat $HELPERCFG 2> /dev/null |grep URL |awk '{print $2}'`
fi
}
doGetOpt()
{
while getopts ":s:d:u:p:I:" opt; do
case $opt in
s)
server=$OPTARG
;;
d)
host=$OPTARG
;;
u)
user=$OPTARG
;;
p)
pass=$OPTARG
;;
I)
if [ "$OPTARG" != "$EMPTYSTRING" ];then
IPURL=$OPTARG
else
IPURL=""
fi
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
}
doWriteCFG()
{
#always write to the inactive config - needs to be enabled vi "start" command later
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
rm $STATUSFILE
#find the interface (always the default gateway interface)
DEFAULT=`ip route |grep default |awk '{print $5}'`
#store the given options in ez-ipupdate compatible config file
echo "host=$host" > $FILE
echo "server=$server" >> $FILE
echo "user=${user}:${pass}" >> $FILE
echo "service-type=gnudip" >> $FILE
echo "retrys=5" >> $FILE
echo "execute=$0 success" >> $FILE
echo "wildcard" >> $FILE
#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 | 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
echo "interface=$DEFAULT" >> $FILE
#if this line is added to config file, ez-ipupdate will be launched on startup via init.d
echo "daemon" >> $FILE
fi
fi
}
doGetWANIP()
{
if [ $IPURL ];then
OUTFILE=`mktemp`
wget -4 -o /dev/null -O $OUTFILE $IPURL
WANIP=`cat $OUTFILE`
rm $OUTFILE
else
#no WAN IP found because of missing check URL
WANIP=${NOIP}
fi
echo $WANIP > $IPFILE
}
cmd=$1
shift
case $cmd in
configure)
doGetOpt $@
doWriteCFG
;;
start)
if [ "$(cat $HELPERCFG |grep ^NAT | awk '{print $2}')" = "no" ];then
mv $CFG_disabled $CFG
/etc/init.d/${TOOLNAME} start
else
echo "*/${UPDATEMINUTES} * * * * root $0 update" > $CRONJOB
$0 update
fi
;;
get-nat)
NAT=`cat $HELPERCFG 2> /dev/null |grep ^NAT | awk '{print $2}'`
[ -z $NAT ] && NAT="unknown"
echo $NAT
;;
update)
doReadCFG
OLDIP=`cat $IPFILE`
doGetWANIP
[ -f $CFG_disabled ] && FILE=$CFG_disabled
[ -f $CFG ] && FILE=$CFG
RESULT=0
#if we know our WAN IP, only update if IP changes
if [ "$OLDIP" != "$WANIP" -a "${WANIP}" != ${NOIP} ];then
${UPDATE_TOOL} -c $FILE
RESULT=$?
cat /proc/uptime |awk '{print $1}' > $LASTUPDATE
fi
#if we don't know our WAN IP do a blind update once a hour
if [ "${WANIP}" = ${NOIP} ];then
UPTIME=`cat /proc/uptime`
LAST=`cat $LASTUPDATE`
DIFF=`expr $UPTIME - $LAST`
if [ $DIFF -gt $UPDATEMINUTESUNKNOWN ];then
${UPDATE_TOOL} -c $FILE
RESULT=$?
cat /proc/uptime |awk '{print $1}' > $LASTUPDATE
fi
fi
if [ $RESULT -eq 0 ];then
$0 success
else
$0 failed
fi
;;
stop)
rm $CRONJOB 2> /dev/null
/etc/init.d/${TOOLNAME} stop
mv $CFG $CFG_disabled
;;
success)
DATE=`date`
echo "last update done ($DATE)" > $STATUSFILE
;;
failed)
DATE=`date`
echo "last update failed ($DATE)" > $STATUSFILE
;;
get-last-success)
if [ -f $STATUSFILE ];then
cat $STATUSFILE
else
echo "no update recorded since last config change"
fi
;;
get-installed)
if [ ! -z ${UPDATE_TOOL} ];then
echo "installed"
fi
;;
status)
doReadCFG
PROC=`pgrep ${TOOLNAME}`
if [ -f $CRONJOB ];then
echo enabled
elif [ ! -z $PROC ];then
echo enabled
else
echo disabled
fi
echo $server
echo $host
echo $user
echo $pass
echo $IPURL
;;
get-timer)
echo $UPDATEMINUTES
;;
clean)
rm ${CFGDIR}/*
;;
*)
echo "usage: get-installed|status|configure <options>|start|stop|update|get-nat|clean|success|failed"
echo ""
echo "options are:"
echo "-s <server> Gnudip Server address"
echo "-d <domain> Domain to be updated"
echo "-u <user> Account username"
echo "-p <password> Account Password"
echo "-I <IP check URL> A URL which returns the IP of the client who is requesting"
echo ""
echo "update do a one time update"
echo "clean delete configuration"
echo "success store update success"
echo "failed store update failure"
;;
esac
exit 0

View File

@ -0,0 +1 @@
plinth.modules.dynamicDNS

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
Plinth module to configure XMPP server
"""
from . import dynamicDNS
from .dynamicDNS import init
__all__ = ['xmpp2', 'init']
depends = ['plinth.modules.apps']

View File

@ -0,0 +1,245 @@
#
# 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 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
LOGGER = logging.getLogger(__name__)
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('dynamicDNS', 'glyphicon-comment', 'dynamicDNS:index', 40)
@login_required
def index(request):
"""Serve dynamic DNS page"""
is_installed = actions.run('dynamicDNS', ['get-installed']).strip() == 'installed'
if is_installed:
index_subsubmenu = subsubmenu
else:
index_subsubmenu = None
return TemplateResponse(request, 'dynamicDNS.html',
{'title': _('dynamicDNS'),
'is_installed': is_installed,
'subsubmenu': index_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"""
enabled = forms.BooleanField(label=_('Enable dynamicDNS'),
required=False)
dynamicDNS_Server = TrimmedCharField(
label=_('Server Address'),
help_text=_('Example: gnudip.provider.org'),
validators=[
validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$',
_('Invalid server name'))])
dynamicDNS_Domain = TrimmedCharField(
label=_('Domain Name'),
help_text=_('Example: hostname.sds-ip.de'),
validators=[
validators.RegexValidator(r'^[\w-]{1,63}(\.[\w-]{1,63})*$',
_('Invalid domain name'))])
dynamicDNS_User = TrimmedCharField(
label=_('Username'),
help_text=_('You should have been requested to select a username when you created the account'))
dynamicDNS_Secret = TrimmedCharField(
label=_('Password'), widget=forms.PasswordInput(),
required=False,
help_text=_('You should have been requested to select a password when you created the account. \
If you left this field empty your password will be unchanged.'))
dynamicDNS_Secret_repeat = TrimmedCharField(
label=_('repeat Password'), widget=forms.PasswordInput(),
required=False,
help_text=_('insert the password twice to avoid typos. If you left this field empty your password \
will be unchanged.'),)
dynamicDNS_IPURL = TrimmedCharField(
label=_('IP check URL'),
required=False,
help_text=_('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'),
validators=[
validators.URLValidator(schemes=['http','https','ftp'])])
@login_required
def configure(request):
"""Serve the configuration form"""
status = get_status()
form = None
if request.method == 'POST':
form = ConfigureForm(request.POST, prefix='dynamicDNS')
if form.is_valid():
_apply_changes(request, status, form.cleaned_data)
status = get_status()
form = ConfigureForm(initial=status, prefix='dynamicDNS')
else:
form = ConfigureForm(initial=status, prefix='dynamicDNS')
return TemplateResponse(request, 'dynamicDNS_configure.html',
{'title': _('Configure dynamicDNS Client'),
'form': form,
'subsubmenu': subsubmenu})
@login_required
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:
status['dynamicDNS_Server'] = details[1]
else:
status['dynamicDNS_Server'] = ''
if len(details) > 2:
status['dynamicDNS_Domain'] = details[2]
else:
status['dynamicDNS_Domain'] = ''
if len(details) > 3:
status['dynamicDNS_User'] = details[3]
else:
status['dynamicDNS_User'] = ''
if len(details) > 4:
status['dynamicDNS_Secret'] = details[4]
else:
status['dynamicDNS_Secret'] = ''
if len(details) > 5:
status['dynamicDNS_IPURL'] = details[5]
else:
status['dynamicDNS_Secret'] = ''
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)
FAIL = False
if new_status['dynamicDNS_Secret_repeat'] != new_status['dynamicDNS_Secret']:
messages.error(request, _('passwords does not match'))
FAIL = True
if old_status['dynamicDNS_Secret'] == '' and new_status['dynamicDNS_Secret'] == '':
messages.error(request, _('please give a password'))
FAIL = True
if False == FAIL:
if new_status['dynamicDNS_Secret'] == '':
new_status['dynamicDNS_Secret'] = old_status['dynamicDNS_Secret']
if new_status['dynamicDNS_IPURL'] == '':
new_status['dynamicDNS_IPURL'] = 'none'
if old_status['dynamicDNS_Server'] != new_status['dynamicDNS_Server'] or \
old_status['dynamicDNS_Domain'] != new_status['dynamicDNS_Domain'] or \
old_status['dynamicDNS_User'] != new_status['dynamicDNS_User'] or \
old_status['dynamicDNS_Secret'] != new_status['dynamicDNS_Secret'] or \
old_status['dynamicDNS_IPURL'] != new_status['dynamicDNS_IPURL']:
_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']])
_run(['stop'])
_run(['start'])
messages.success(request, _('Dynamic DNS configuration is updated!'))
if old_status['enabled'] != new_status['enabled']:
if new_status['enabled']:
_run(['start'])
messages.success(request, _('Dynamic DNS is enabled now!'))
else:
_run(['stop'])
messages.success(request, _('Dynamic DNS is disabled now!'))
else:
messages.error(request, _('At least on failure occured, please check your input.'))
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block content %}
{% if is_installed %}
<p>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 <a href='http://gnudip2.sourceforge.net/'
target='_blank'> gnudip </a> server. Afterwards the Server will assign your personal DNS name with the new IP and if someone from the internet asks for your DNS name he will get your personal IP answered.</p>
{% else %}
<h2>DynamicDNS client</h2>
<p>The Dynamic DNS tool <i>ez-ipupdate</i> is not installed.</p>
<p>ez-ipupdate comes pre-installed with {{ cfg.box_name }}. On any Debian-based
system (such as {{ cfg.box_name }}) you may install it using the command
<code>apt-get install ez-ipupdate</code>.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,34 @@
{% 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 %}
<h3>Configuration</h3>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary btn-md" value="Update setup"/>
</form>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block content %}
<p>
<h3>NAT type</h3>
{% 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 %}
<h3>Last update</h3>
{{ last_update }}
</p>
{% endblock %}

View File

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