#!/usr/bin/python
# -*- coding: iso-8859-1 -*-

"""
A small CGI to politely inform people where they are and open
up the firewall for them.
"""

import cgi
import os
import sys
import re
import time

############ Config

wlanAddress = "10.10.0.1"
wlanNetwork = "10.10.0.0/24"
wlanInterface = "eth1"
outInterface = "ppp0"
senderAddress = "postmaster@foo.org"
installPath = os.path.abspath(os.path.dirname(sys.argv[0]))
helperName = "cportiphelper"
operatorAddress = os.environ.get("CAP_OPERATOR")

initMACs = ["00:03:0d:30:57:a4", "28:b2:bd:cf:f7:0b",
	"74:2f:68:2d:5a:e9", "d8:75:33:32:ff:db"]

############# CGI business

welcomeMessage = """Content-type: text/html; charset=iso-8859-1
Connection: close

<head>
<title>Offenes WLAN</title>
<style type="text/css">
div#germanintro {
	width:300px;
	display:table-cell;
	padding: 10px;
}

div#englishintro {
	width:300px;
	display:table-cell;
	padding: 10px;
}

div#intro{
	display: table-row;
}
</style>
</head>
<body>
<div id="intro">
<div id="germanintro">
<h1>Hallo auch,</h1>
<p>Schön, dich hier zu sehen.  Weil ich mich freue, wenn es irgendwo
offene WLANs gibt, lasse ich auch das hier offen.  Das Ganze läuft aber
nur, wenn alle eine Regel befolgen: <strong>Mach über dieses WLAN
nichts, was du nicht über dein eigenes machen würdest</strong>.</p>

<p>Es gibt genug Methoden, der Vorratsdatenspeicherung ein Schnippchen zu
schlagen, ohne wen anders in Schwierigkeiten zu bringen.  Wenn du Nachbar bist,
willst du vielleicht überlegen, ob du dir irgendwann mal einen eigenen
Netzzugang besorgst -- gegen gelegentliches Mailziehen hat hier zwar niemand
was, gewaltige Downloads oder Netzglotze finden wir dagegen auf Dauer nicht 
so toll.</p>

<p>Warnung: Ich behalte mir vor, dann und wann mal nach dem Rechten zu sehen.
Bitte verschlüssele also Daten, von denen du willst, dass sie niemand anders
sieht (das solltest du ohnehin tun).</p>
</div>

<div id="englishintro">
<h1>Hi there,</h1>

<p>Nice to see you. You're welcome to use this WLAN. Please don't do anything
from here that you wouldn't do from your own net connection.  We don't want to
get into trouble and, this being Germany, the limit is not that terribly
high.</p>

<p>Warning: I may be checking what's going on on the net now and then.
You should encrypt data you don't want other people to see (but that's
something you should do anyway).
</div>
</div>

<form action="/letmein"
    onSubmit="document.getElementById('nextAddr').value = document.URL">
  <p>Drop me a note if you like:</p>
  <textarea name="message" cols="70" rows="5"></textarea>
	<p>And if you'd want me to respond, you can (but don't need to)
	enter a mail address I can reach you at:</p>
	<p><input type="text" name="userAddr" size="70"></p>
  <p><input type="hidden" name="nextAddr" value="" id="nextAddr"></p>
  <p><input type="submit" value="Let me in, then"></p>
</form>
</body>
"""

errorTemplate = """Content-type: text/html; charset=iso-8859-1
Connection: close

<head><title>Error message</title></head>
<body><h1>An error has occurred</h1>
<p>Sorry -- something went wrong.  My trouble was:</p>
<pre>
%(msg)s
</pre>
<p>If you can't fix this yourself, you try Steubenstraße 28,
Brämer, Demleitner, Weineck.  Ask for Markus, he may be able to help
you.</p>
</body>
"""

successTemplate = """Content-type: text/html; charset=iso-8859-1
Refresh: 10;%(nextAddr)s
Connection: close

<head><title>Success</title></head>
<body>
<h1>Welcome</h1>
<p>You should now be able to access the net.  You will be redirected
to <a href="%(nextAddr)s">%(nextAddr)s</a> in 10 seconds.</p>
</body>
"""

class Error(Exception):
	pass

def showError(msg):
	print errorTemplate%{"msg": msg}

def getRemoteMac():
	try:
		f = os.popen("/usr/sbin/arp -an -i %s '%s'"%(wlanInterface,
			os.environ["REMOTE_ADDR"]))
		s = f.read()
		if f.close():
			raise Error("arp execution failed")
		return re.search("at ([\da-fA-F:]{17}) ", s).group(1)
	except Exception, msg:
		raise Error("Could not determine your MAC address (%s)"%
			msg)

def sendMailTo(user, mailtext):
	message = """From: User of open WLAN <%s>
Content-Type: text/plain; charset=iso-8859-1
To: %s
Date: %s

%s
"""%(senderAddress, user, time.asctime(), mailtext)
	os.popen("/usr/sbin/sendmail -i -t", "w").write(message)

def mailToOp(msg):
	if not msg or not msg.strip() or not operatorAddress:
		return
	sendMailTo(operatorAddress, msg)


def insertFwRule(remoteMac):
	# remoteMac comes from arp and thus should be safe.
	if os.system("%s '%s'"%(os.path.join(installPath, helperName), remoteMac)):
		raise Error("Insert of firewall rule failed.")


def openUpFirewall():
	remoteMac = getRemoteMac()
	insertFwRule(remoteMac)
	form = cgi.FieldStorage()
	mailToOp("Original sender claimed to be: %s\n\n%s"%(
		form.getfirst("userAddr"),
		form.getfirst("message")))
	nextAddr = form.getfirst("nextAddr", "http://www.tfiu.de")
	print successTemplate%{
		"nextAddr": nextAddr,
	}


############# stuff for initial build ("make")

helperSource = """
/* machine generated, do not edit */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void validate(char *s)
{
	char *c;

	for (c=s; *c; c++) {
		if (!isxdigit(*c) && *c!=':') {
			fprintf(stderr, "%s does not look like a MAC address.\\n", s);
			exit(1);
		}
	}
}

int main(int argc, char **argv)
{
	char *nullEnv[] = {NULL};

	if (argc!=2) {
		fprintf(stderr, "Don't call this program directly, it's a helper for"
			" the capturing portal.\\n");
		exit(1);
	}
	validate(argv[1]);
	/* hardcoding iptable's path -- you may need to change this in the
	python source. */
	if (execle("/sbin/iptables", "/sbin/iptables", "-t", "nat", "-I", 
			"PREROUTING", "-m", "mac", "--mac-source", argv[1], "-j", "ACCEPT",
			(char*)NULL, nullEnv)) {
		perror("Capturing portal helper failed: ");
	}
	return 1;
}
"""

operatorMessage = """
----------------- READ THIS: -------------------

You should now set the resulting binary cportiphelper suid root:

sudo chown root cportiphelper
sudo chmod u+s cportiphelper

You can inspect its source in openFw's helperSource variable.

After that, just tell apache where your capturing portal resides.
The stanza might look like this:

<VirtualHost %(wlanAddress)s>
	# DocumentRoot doesn't matter, nothing will be served from there.
	DocumentRoot /var/www/wlan  
	ScriptAlias / %(installPath)s/openFw/
</VirtualHost>

To receive the mails people may leave on the portal, add something
like

	SetEnv CAP_OPERATOR somerandom@mail.address.foo

in there, replacing the obivious.
"""

def initialBuild():
	"""builds the binary for inserting firewall rules and informs the
	user.

	This only needs to be run at installation time.
	"""
	f = open(helperName+".c", "w")
	f.write(helperSource)
	f.close()
	if os.system("make %s"%helperName):
		raise Error("Something went wrong when building helper.")
	print operatorMessage%globals()


############## trivial init.d interface

def setupIptables(bringUp):
	action = {True: "-A", False: "-D"}[bringUp]
	# DNS queries to avoid poisoning the client's DNS cache
	os.system("iptables -t nat %s POSTROUTING -o %s"
		" -j MASQUERADE"%(action, outInterface))
	os.system("iptables -t nat %s PREROUTING -s %s"
		" -p tcp --destination-port 53 -j ACCEPT"%(action, wlanNetwork))
	os.system("iptables -t nat %s PREROUTING -s %s"
		" -p udp --destination-port 53 -j ACCEPT"%(action, wlanNetwork))
	# access to self
	os.system("iptables -t nat %s PREROUTING -s %s"
		" -j DNAT --to-destination %s"%(action, wlanNetwork, wlanAddress))


def setupDefaultRules():
	setupIptables(True)


def teardownDefaultRules():
	setupIptables(False)


############## "User interface"

def cgimain():
	if os.environ["REQUEST_URI"].split('?')[0].strip("/")=="letmein":
		try:
			openUpFirewall()
		except Exception, msg:
			showError(msg)
	else:
		print welcomeMessage


def usermain():
	if sys.argv[1]=="start":
		setupDefaultRules()
		for mac in initMACs:
			insertFwRule(mac)
	elif sys.argv[1]=="stop":
		teardownDefaultRules()
	elif sys.argv[1]=="make":
		initialBuild()
	else:
		raise Error("Usage: %s [start|stop|make]")


if __name__=="__main__":
	if len(sys.argv)==1:
		cgimain()
	else:
		usermain()
