--- /dev/null
+*.mo
+*.py[co]
+ddns/__version__.py
--- /dev/null
+[main]
+host = https://www.transifex.net
+
+[ipfire.dynamic-dns-client]
+file_filter = po/<lang>.po
+source_file = po/ddns.pot
+source_lang = en
--- /dev/null
+
+PACKAGE_NAME = ddns
+PACKAGE_VERSION = 0.01
+
+DESTDIR =
+PREFIX = /usr
+SYSCONFDIR = /etc
+BINDIR = $(PREFIX)/bin
+LIBDIR = $(PREFIX)/lib
+LOCALEDIR = $(PREFIX)/share/locale
+
+# Get the version and configuration of the python interpreter.
+PYTHON_VERSION = $(shell python -c "import platform; print '.'.join(platform.python_version_tuple()[:2])")
+ifeq "$(PYTHON_VERSION)" ""
+ $(error Could not determine the version of the python interpreter.)
+endif
+PYTHON_DIR = $(LIBDIR)/python$(PYTHON_VERSION)/site-packages/ddns
+
+VERSION_FILE = ddns/__version__.py
+
+###
+# Translation stuff
+###
+# A list of all files that need translation
+TRANSLATION_FILES = $(wildcard ddns/*.py) ddns.py
+
+POT_FILE = po/$(PACKAGE_NAME).pot
+PO_FILES = $(wildcard po/*.po)
+MO_FILES = $(patsubst %.po,%.mo,$(PO_FILES))
+
+################################################################################
+
+all: $(POT_FILE) $(MO_FILES)
+ @: # Do nothing else.
+
+$(VERSION_FILE): Makefile
+ echo "# this file is autogenerated by the build system" > $(VERSION_FILE)
+ echo "CLIENT_VERSION = \"$(PACKAGE_VERSION)\"" >> $(VERSION_FILE)
+
+install: $(VERSION_FILE) $(MO_FILES)
+ # Install the main command.
+ -mkdir -pv $(DESTDIR)$(BINDIR)
+ install -v -m 755 ddns.py $(DESTDIR)$(BINDIR)/ddns
+
+ # Install python module.
+ -mkdir -pv $(DESTDIR)$(PYTHON_DIR)
+ install -v -m 644 ddns/*.py $(DESTDIR)$(PYTHON_DIR)
+
+ # Install the example configuration file.
+ -mkdir -pv $(DESTDIR)$(SYSCONFDIR)
+ install -v -m 640 ddns.conf $(DESTDIR)$(SYSCONFDIR)/ddns.conf
+
+ # Install translation files.
+ -mkdir -pv $(DESTDIR)$(LOCALEDIR)
+ for file in $(MO_FILES); do \
+ lang=$${file/.mo/}; \
+ mkdir -pv $(DESTDIR)$(LOCALEDIR)/$${lang}/LC_MESSAGES; \
+ install -v -m 644 $${file} \
+ $(DESTDIR)$(LOCALEDIR)/$${lang}/LC_MESSAGES/$(PACKAGE_NAME).mo; \
+ done
+
+# Cleanup temporary files.
+clean:
+ rm -f $(VERSION_FILE)
+ rm -f $(MO_FILES)
+
+# Translation stuff.
+$(POT_FILE): $(TRANSLATION_FILES) Makefile
+ xgettext --language python -d $(PACKAGE_NAME) -k_ -kN_ \
+ -o $@ --add-comments --from-code=UTF-8 $^
+
+# Compile gettext dictionaries from translation files.
+%.mo: %.po $(POT_FILE)
+ msgfmt -o $@ $<
--- /dev/null
+#
+# This is a sample configuration file for the
+# IPFire dynamic DNS update client.
+#
+
+[config]
+
+# Upstream proxy.
+# proxy = http://192.168.180.1:800
+
+# Guess the external IP address with help
+# of an external server.
+# guess_external_ip = true
+
+# Accounts are its own sections.
+# These are some examples.
+
+# [test.no-ip.org]
+# provider = no-ip.com
+# username = user
+# password = pass
+
+# [test.selfhost.de]
+# provider = selfhost.de
+# username = user
+# password = pass
--- /dev/null
+#!/usr/bin/python
+
+import ddns
+
+d = ddns.DDNSCore(debug=1)
+d.load_configuration("/etc/ddns.conf")
+
+d.updateall()
--- /dev/null
+#!/usr/bin/python
+
+import logging
+import logging.handlers
+import ConfigParser
+
+from i18n import _
+
+logger = logging.getLogger("ddns.core")
+logger.propagate = 1
+
+from .providers import *
+from .system import DDNSSystem
+
+# Setup the logger.
+def setup_logging():
+ rootlogger = logging.getLogger("ddns")
+ rootlogger.setLevel(logging.DEBUG)
+
+ # Setup a logger that logs to syslog.
+ #handler = logging.handlers.SysLogHandler(address="/dev/log")
+
+ handler = logging.StreamHandler()
+ rootlogger.addHandler(handler)
+
+setup_logging()
+
+class DDNSCore(object):
+ def __init__(self, debug=False):
+ # In debug mode, enable debug logging.
+ if debug:
+ logger.setLevel(logging.DEBUG)
+
+ # Initialize the settings array.
+ self.settings = {}
+
+ # Dict with all providers, that are supported.
+ self.providers = {}
+ self.register_all_providers()
+
+ # List of configuration entries.
+ self.entries = []
+
+ # Add the system class.
+ self.system = DDNSSystem(self)
+
+ def register_provider(self, provider):
+ """
+ Registers a new provider.
+ """
+ assert issubclass(provider, DDNSProvider)
+
+ provider_handle = provider.INFO.get("handle")
+ assert provider_handle
+
+ assert not self.providers.has_key(provider_handle), \
+ "Provider '%s' has already been registered" % provider_handle
+
+ provider_name = provider.INFO.get("name")
+ assert provider_name
+
+ logger.debug("Registered new provider: %s (%s)" % (provider_name, provider_handle))
+ self.providers[provider_handle] = provider
+
+ def register_all_providers(self):
+ """
+ Simply registers all providers.
+ """
+ for provider in (
+ DDNSProviderNOIP,
+ DDNSProviderSelfhost,
+ ):
+ self.register_provider(provider)
+
+ def load_configuration(self, filename):
+ configs = ConfigParser.SafeConfigParser()
+ configs.read([filename,])
+
+ # First apply all global configuration settings.
+ for k, v in configs.items("config"):
+ self.settings[k] = v
+
+ for entry in configs.sections():
+ # Skip the special config section.
+ if entry == "config":
+ continue
+
+ settings = {}
+ for k, v in configs.items(entry):
+ settings[k] = v
+ settings["hostname"] = entry
+
+ # Get the name of the provider.
+ provider = settings.get("provider", None)
+ if not provider:
+ logger.warning("Entry '%s' lacks a provider setting. Skipping." % entry)
+ continue
+
+ # Try to find the provider with the wanted name.
+ try:
+ provider = self.providers[provider]
+ except KeyError:
+ logger.warning("Could not find provider '%s' for entry '%s'." % (provider, entry))
+ continue
+
+ # Create an instance of the provider object with settings from the
+ # configuration file.
+ entry = provider(self, **settings)
+
+ # Add new entry to list (if not already exists).
+ if not entry in self.entries:
+ self.entries.append(entry)
+
+ def updateall(self):
+ # If there are no entries, there is nothing to do.
+ if not self.entries:
+ logger.debug(_("Found no entries in the configuration file. Exiting."))
+ return
+
+ # Update them all.
+ for entry in self.entries:
+ self.update(entry)
+
+ def update(self, entry):
+ try:
+ entry()
+
+ except DDNSUpdateError, e:
+ logger.error(_("Dynamic DNS update for %(hostname)s (%(provider)s) failed:") % \
+ { "hostname" : entry.hostname, "provider" : entry.name })
+ logger.error(" %s" % e)
+
+ except Exception, e:
+ logger.error(_("Dynamic DNS update for %(hostname)s (%(provider)s) throwed an unhandled exception:") % \
+ { "hostname" : entry.hostname, "provider" : entry.name })
+ logger.error(" %s" % e)
+
+ logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
+ { "hostname" : entry.hostname, "provider" : entry.name })
--- /dev/null
+#!/usr/bin/python
+
+class DDNSError(Exception):
+ pass
+
+
+class DDNSAbuseError(DDNSError):
+ """
+ Thrown when the server reports
+ abuse for this account.
+ """
+ pass
+
+
+class DDNSAuthenticationError(DDNSError):
+ """
+ Thrown when the server did not
+ accept the user credentials.
+ """
+ pass
+
+
+class DDNSInternalServerError(DDNSError):
+ """
+ Thrown when the remote server reported
+ an error on the provider site.
+ """
+ pass
+
+
+class DDNSRequestError(DDNSError):
+ """
+ Thrown when a request could
+ not be properly performed.
+ """
+ pass
+
+
+class DDNSUpdateError(DDNSError):
+ """
+ Thrown when an update could not be
+ properly performed.
+ """
+ pass
--- /dev/null
+#!/usr/bin/python
+
+import gettext
+
+TEXTDOMAIN = "ddns"
+
+N_ = lambda x: x
+
+def _(singular, plural=None, n=None):
+ """
+ A function that returnes the translation of a string if available.
+
+ The language is taken from the system environment.
+ """
+ if not plural is None:
+ assert n is not None
+ return gettext.dngettext(TEXTDOMAIN, singular, plural, n)
+
+ return gettext.dgettext(TEXTDOMAIN, singular)
+
--- /dev/null
+#!/usr/bin/python
+
+# Import all possible exception types.
+from .errors import *
+
+class DDNSProvider(object):
+ INFO = {
+ # A short string that uniquely identifies
+ # this provider.
+ "handle" : None,
+
+ # The full name of the provider.
+ "name" : None,
+
+ # A weburl to the homepage of the provider.
+ # (Where to register a new account?)
+ "website" : None,
+
+ # A list of supported protocols.
+ "protocols" : ["ipv6", "ipv4"],
+ }
+
+ DEFAULT_SETTINGS = {}
+
+ def __init__(self, core, **settings):
+ self.core = core
+
+ # Copy a set of default settings and
+ # update them by those from the configuration file.
+ self.settings = self.DEFAULT_SETTINGS.copy()
+ self.settings.update(settings)
+
+ def __repr__(self):
+ return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
+
+ def __cmp__(self, other):
+ return cmp(self.hostname, other.hostname)
+
+ @property
+ def name(self):
+ """
+ Returns the name of the provider.
+ """
+ return self.INFO.get("name")
+
+ @property
+ def website(self):
+ """
+ Returns the website URL of the provider
+ or None if that is not available.
+ """
+ return self.INFO.get("website", None)
+
+ @property
+ def handle(self):
+ """
+ Returns the handle of this provider.
+ """
+ return self.INFO.get("handle")
+
+ def get(self, key, default=None):
+ """
+ Get a setting from the settings dictionary.
+ """
+ return self.settings.get(key, default)
+
+ @property
+ def hostname(self):
+ """
+ Fast access to the hostname.
+ """
+ return self.get("hostname")
+
+ @property
+ def username(self):
+ """
+ Fast access to the username.
+ """
+ return self.get("username")
+
+ @property
+ def password(self):
+ """
+ Fast access to the password.
+ """
+ return self.get("password")
+
+ def __call__(self):
+ raise NotImplementedError
+
+ def send_request(self, *args, **kwargs):
+ """
+ Proxy connection to the send request
+ method.
+ """
+ return self.core.system.send_request(*args, **kwargs)
+
+ def get_address(self, proto):
+ """
+ Proxy method to get the current IP address.
+ """
+ return self.core.system.get_address(proto)
+
+
+class DDNSProviderNOIP(DDNSProvider):
+ INFO = {
+ "handle" : "no-ip.com",
+ "name" : "No-IP",
+ "website" : "http://www.no-ip.com/",
+ "protocols" : ["ipv4",]
+ }
+
+ # Information about the format of the HTTP request is to be found
+ # here: http://www.no-ip.com/integrate/request and
+ # here: http://www.no-ip.com/integrate/response
+
+ url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update?hostname=%(hostname)s&myip=%(address)s"
+
+ def __call__(self):
+ url = self.url % {
+ "hostname" : self.hostname,
+ "username" : self.username,
+ "password" : self.password,
+ "address" : self.get_address("ipv4"),
+ }
+
+ # Send update to the server.
+ response = self.send_request(url)
+
+ # Get the full response message.
+ output = response.read()
+
+ # Handle success messages.
+ if output.startswith("good") or output.startswith("nochg"):
+ return
+
+ # Handle error codes.
+ if output == "badauth":
+ raise DDNSAuthenticationError
+ elif output == "aduse":
+ raise DDNSAbuseError
+ elif output == "911":
+ raise DDNSInternalServerError
+
+ # If we got here, some other update error happened.
+ raise DDNSUpdateError
+
+
+class DDNSProviderSelfhost(DDNSProvider):
+ INFO = {
+ "handle" : "selfhost.de",
+ "name" : "Selfhost.de",
+ "website" : "http://www.selfhost.de/",
+ "protocols" : ["ipv4",],
+ }
+
+ url = "https://carol.selfhost.de/update?username=%(username)s&password=%(password)s&textmodi=1"
+
+ def __call__(self):
+ url = self.url % { "username" : self.username, "password" : self.password }
+
+ response = self.send_request(url)
+
+ match = re.search("status=20(0|4)", response.read())
+ if not match:
+ raise DDNSUpdateError
--- /dev/null
+#!/usr/bin/python
+
+import re
+import urllib2
+
+from __version__ import CLIENT_VERSION
+from i18n import _
+
+# Initialize the logger.
+import logging
+logger = logging.getLogger("ddns.system")
+logger.propagate = 1
+
+class DDNSSystem(object):
+ """
+ The DDNSSystem class adds a layer of abstraction
+ between the ddns software and the system.
+ """
+
+ # The default useragent.
+ USER_AGENT = "IPFireDDNSUpdater/%s" % CLIENT_VERSION
+
+ def __init__(self, core):
+ # Connection to the core of the program.
+ self.core = core
+
+ @property
+ def proxy(self):
+ proxy = self.core.settings.get("proxy")
+
+ # Strip http:// at the beginning.
+ if proxy.startswith("http://"):
+ proxy = proxy[7:]
+
+ return proxy
+
+ def guess_external_ipv4_address(self):
+ """
+ Sends a request to the internet to determine
+ the public IP address.
+
+ XXX does not work for IPv6.
+ """
+ response = self.send_request("http://checkip.dyndns.org/")
+
+ if response.code == 200:
+ match = re.search(r"Current IP Address: (\d+.\d+.\d+.\d+)", response.read())
+ if match is None:
+ return
+
+ return match.group(1)
+
+ def send_request(self, url, data=None, timeout=30):
+ logger.debug("Sending request: %s" % url)
+ if data:
+ logger.debug(" data: %s" % data)
+
+ req = urllib2.Request(url, data=data)
+
+ # Set the user agent.
+ req.add_header("User-Agent", self.USER_AGENT)
+
+ # All requests should not be cached anywhere.
+ req.add_header("Pragma", "no-cache")
+
+ # Set the upstream proxy if needed.
+ if self.proxy:
+ logger.debug("Using proxy: %s" % self.proxy)
+
+ # Configure the proxy for this request.
+ req.set_proxy(self.proxy, "http")
+
+ logger.debug(_("Request header:"))
+ for k, v in req.headers.items():
+ logger.debug(" %s: %s" % (k, v))
+
+ try:
+ resp = urllib2.urlopen(req)
+
+ # Log response header.
+ logger.debug(_("Response header:"))
+ for k, v in resp.info().items():
+ logger.debug(" %s: %s" % (k, v))
+
+ # Return the entire response object.
+ return resp
+
+ except urllib2.URLError, e:
+ raise
+
+ def get_address(self, proto):
+ assert proto in ("ipv6", "ipv4")
+
+ if proto == "ipv4":
+ # Check if the external IP address should be guessed from
+ # a remote server.
+ guess_ip = self.core.settings.get("guess_external_ip", "")
+
+ # If the external IP address should be used, we just do
+ # that.
+ if guess_ip in ("true", "yes", "1"):
+ return self.guess_external_ipv4_address()
+
+ # XXX TODO
+ assert False
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-07-19 18:48+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ddns/__init__.py:117
+msgid "Found no entries in the configuration file. Exiting."
+msgstr ""
+
+#: ddns/__init__.py:129
+#, python-format
+msgid "Dynamic DNS update for %(hostname)s (%(provider)s) failed:"
+msgstr ""
+
+#: ddns/__init__.py:134
+#, python-format
+msgid ""
+"Dynamic DNS update for %(hostname)s (%(provider)s) throwed an unhandled "
+"exception:"
+msgstr ""
+
+#: ddns/__init__.py:138
+#, python-format
+msgid "Dynamic DNS update for %(hostname)s (%(provider)s) successful"
+msgstr ""
+
+#: ddns/system.py:73
+msgid "Request header:"
+msgstr ""
+
+#. Log response header.
+#: ddns/system.py:81
+msgid "Response header:"
+msgstr ""
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Michael Tremer <michael.tremer@ipfire.org>, 2012.
+msgid ""
+msgstr ""
+"Project-Id-Version: The IPFire Project\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-07-19 17:03+0000\n"
+"PO-Revision-Date: 2012-07-19 17:11+0000\n"
+"Last-Translator: Michael Tremer <michael.tremer@ipfire.org>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/ipfire/language/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: ddns/__init__.py:117
+msgid "Found no entries in the configuration file. Exiting."
+msgstr "Keine Einträge in der Konfigurationsdatei gefunden. Ende."
+
+#: ddns/__init__.py:126
+#, python-format
+msgid "Dynamic DNS update for %(hostname)s (%(provider)s) failed:"
+msgstr "Dynamic DNS-Update für %(hostname)s (%(provider)s) fehlgeschlagen"
+
+#: ddns/__init__.py:131
+#, python-format
+msgid ""
+"Dynamic DNS update for %(hostname)s (%(provider)s) throwed an unhandled "
+"exception:"
+msgstr "Dynamic DNS-Update für %(hostname)s (%(provider)s) erzeugte einen unerwarteten Fehler:"
+
+#. XXX DEBUG
+#: ddns/__init__.py:136
+#, python-format
+msgid "Dynamic DNS update for %(hostname)s (%(provider)s) successful"
+msgstr "Dynamic DNS-Update für %(hostname)s (%(provider)s) erfolgreich"
+
+#: ddns/system.py:73
+msgid "Request header:"
+msgstr "Request-Header:"
+
+#. Log response header.
+#: ddns/system.py:81
+msgid "Response header:"
+msgstr "Response-Header:"