#!/usr/bin/env python
"""
A little piece of CGI to play a dyndns.

Essentially, this takes a "key" from the path, looks for a corresponding
structured comment in a zone file, updates the entry, and triggers
a zone update.

Dependencies (debian package names): apache2, nsd, apache2-suexec-pristine

This does *not* use dns dynamic update but rather opts to hack the
zone file.  Since I need elevated privileges and some sort of access
key definition anyway, I felt I might as well do some old-fashiond
text hacking.

The zone file's name is assumed to be <zone name>.zone (if it's not,
you'll need to change the checK_call command line below).

Configuration (assuming a suitably debianoid setup):

(1) Create an unprivileged user dynnsd: 
	$ sudo adduser --system --disabled-login dynnsd

(2) Create a document root within /var/www for the service and copy
  this script there and make it executable; then grant the whole thing
  to dynnsd
  $ sudo mkdir --mode 755 /var/www/updater
	$ sudo cp /path/to/download/update_zone /var/www/updater/
	$ sudo chmod +x /var/www/updater/update_zone
	$ sudo chown -R dynnsd:nogroup /var/www/updater/

(2) Grant the zonefile to dynnsd:
	$ cd /etc/nsd
	$ sudo chown dynnsd:root tfiu.de.zone  # adapt file name

(3)	Adapt ZONEFILE variable below to point to the file just touched.

(4) Let dynnsd reload the server config:
	$ visudo
	add a line like
	dynnsd ALL=NOPASSWD: /usr/sbin/nsd-control

(5) Make a cname updater.<domain> for where the CGI is going to sit; this
	could look like
	updater.tfiu.de. IN CNAME www.tfiu.de.
	Update your zone as usual.

(6) Drop the following into a file /etc/apache2/sites-enabled/100-updater.conf

	<VirtualHost *:80>
  	ServerName updater
  	ScriptAlias / /var/www/updater/update_zone/
  	SuexecUserGroup dynnsd nogroup
	</VirtualHost>

	Of course, you need to change the ServerName to what
	things happen to be on your end.

(7) Enable the suexec module and restart the server
	$ sudo a2enmod suexec
	$ sudo service apache2 restart

Then, for each host, make a zone file entry:

	dyn.tfiu.de   60 IN A 127.0.0.1 ; DYNNSD:xyz

(only one blank between IN and A, no blank between DYNNSD: and the key.

Then, accessing http://example.com/update_zone/xyz will update the
address on that line to where the request comes from.

The script always returns a text/plain document starting with either
Success: or Error:
"""

import cgi
import os
import subprocess

# ZONEFILE must be be <config_path>/<zone name>.zone or reload won't work.
ZONEFILE = "/etc/nsd/tfiu.de.zone" 


def reply(msg):
	print "content-type: text/plain\n"
	print msg


def set_in_zonefile(host_key, remote_addr):
	lines = []
	sentinel = "DYNNSD:"+host_key+"\n"
	updated = None

	with open(ZONEFILE, "r+") as f:
		for line in f:
			if line.endswith("; Serial\n"):
				lines.append("%d ; Serial\n"%(int(line.split(";")[0])+1))
			elif line.endswith(sentinel):
				prefix, _ = line.split("IN A")
				lines.append("%sIN A %s ; %s"%(
					prefix, remote_addr, sentinel))
				updated = lines[-1]
			else:
				lines.append(line)
		
		if updated:
			f.seek(0)
			f.truncate()
			f.write("".join(lines))

	if updated:
		subprocess.check_output(["sudo", "-n", "nsd-control", "reload",
			os.path.basename(ZONEFILE)[:-5]])
		reply("Success: "+updated)

	else:
		reply("Error: Unknown key")
		

def main():
	host_key = os.environ.get("PATH_INFO", "")[1:]
	remote_addr = os.environ["REMOTE_ADDR"]

	try:
		set_in_zonefile(host_key, remote_addr)
	except Exception, msg:
		import traceback; traceback.print_exc()
		reply("Error: %s"%msg)
	

def _test():
	os.environ["PATH_INFO"] = "/flobbatsch"
	os.environ["REMOTE_ADDR"] = "127.0.0.1"
	main()


if __name__=="__main__":
	#_test()
	main()
