source: hybrid/branches/releng-10/nanobsd/files/usr/local/www/wlportal/index.cgi

Last change on this file was 12947, checked in by huub, 10 years ago

python cgi script moet kennelijk rechtstreeks naar python2.7 verwijzen (?)

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 12.7 KB
Line 
1#!/usr/local/bin/python2.7
2#
3# Wrap me around tcpserver or inetd, example usage for tcpserver (debug):
4# tcpserver -HRl localhost 172.31.255.1 /root/wlportal.py
5#
6# Or put me in a CGI script in for example thttpd server:
7#
8# = Usage =
9# This is a wrapper script which does very basic HTML parsing and altering of
10# pfctl tables rules to build a basic Captive Portal, with basic sanity
11# checking. The ACL is IP based (this is a poor mans solution, layer2
12# ACL would be much better), so don't take security very seriously.
13#
14# To get traffic by default to the portal iI requires a few special rules in
15# pfctl to work properly:
16# no rdr on { $captive_ifs } proto tcp from <wlportal> to !$wl_net port 80
17# rdr on { $captive_ifs } proto tcp from $wl_net to !$wl_net port 80 -> 127.0.0.1 port 8082
18#
19# Enties older than 5 minutes not being used will be removed if the (hidden)
20# argument action=cleanup is given as GET variable. So having this in cron (would fix it):
21# */5 * * * * /usr/bin/fetch -q http://172.31.255.1/wlportal?action=cleanup
22#
23# XXX: The whitelist entries first needs to contact the wlportal.py to get
24# added to the whitelist, this may cause issues during initial setup and hence
25# it might be advised to create a block of static whitelist IP addresses which
26# get added during boot and will never disappear.
27#
28# State : v0.6.0
29# Version : $Id: index.cgi 12947 2014-10-05 09:39:36Z huub $
30# Author : Rick van der Zwet <info@rickvanderzwet.nl>
31# Licence : BSDLike http://wirelessleiden.nl/LICENSE
32
33class MultiTracebackHook:
34 """A hook to replace sys.excepthook that shows tracebacks in syslog & HTML (using cgitb)"""
35 def __init__(self, ident=None, enable=False):
36 self.ident = ident
37 if enable:
38 self.enable()
39
40 def __call__(self, etype, evalue, etb):
41 self.handle((etype, evalue, etb))
42
43 def handle(self, info=None):
44 import cgitb
45 import os
46 import sys
47 import syslog
48 import traceback
49 info = info or sys.exc_info()
50 tb = traceback.format_exception(*info)
51 if self.ident:
52 syslog.openlog(self.ident)
53 prefix = '[%i]' % os.getpid()
54 for line in tb:
55 syslog.syslog(line)
56 cgitb.handler(info)
57
58 def enable(self):
59 import sys
60 sys.excepthook = self
61
62MultiTracebackHook(ident='wlportal', enable=True)
63
64import os
65import signal
66import subprocess
67import socket
68import sys
69import time
70import traceback
71import urlparse
72import yaml
73
74from jinja2 import Template
75
76# XXX: Make me dynamic for example put me in the conf file
77cfg = {
78 'autologin' : False,
79 'cmd_arp' : '/usr/sbin/arp',
80 'pfctl' : '/sbin/pfctl',
81 'portal_sponsor': 'Sponsor van Stichting Wireless Leiden',
82 'portal_url' : 'http://wirelessleiden.nl/welkom?connected_to=%s' % socket.gethostname(),
83 'portalroot' : '172.31.255.1',
84 'refresh_delay' : 3,
85 'tmpl_autologin': '/usr/local/etc/wlportal/autologin.tmpl',
86 'tmpl_login' : '/usr/local/etc/wlportal/login.tmpl',
87 'warnlist' : [],
88 'whitelist' : [],
89 'blacklist' : [],
90 'config_files' : ['/usr/local/etc/wlportal/config.yaml','/usr/local/etc/wleiden.yaml'],
91 'expire_time' : None,
92 'accessdb' : '/var/db/clients',
93 'net_status' : '/tmp/network.status',
94}
95
96
97# No failback if config does not exist, to really make sure the user knows if
98# the config file failed to parse properly or is non-existing
99for config_file in cfg['config_files']:
100 if os.path.isfile(config_file):
101 cfg.update(yaml.load(open(config_file)))
102
103internet_up = True
104if os.path.isfile(cfg['net_status']):
105 internet_up = 'internet=up' in open(cfg['net_status'], 'r').read().lower()
106
107if not internet_up:
108 cfg['warning_msg'] = "<b>Internet Problemen</b>: De laatste 15 minuten zijn er problemen met de (internet) verbinding geconstateerd, de gebruikers ervaring kan dus niet optimaal zijn. Onze excuses voor het eventuele ongemak. Bij aanhoudende problemen kunt u contact opnemen met gebruikers@lijst.wirelessleiden.nl"
109
110def log_registered_host(remote_mac, remote_host):
111 """
112 Write statistics file, used for (nagios) monitoring purposes
113 """
114 with open(cfg['accessdb'],"a") as fh:
115 epoch = int(time.time())
116 fh.write("%s %s %s \n" % (epoch, remote_mac, remote_host) )
117
118class MACnotFound(Exception):
119 pass
120
121def get_mac(ipaddr):
122 """ Find out the MAC for a certain IP address """
123 try:
124 # ? (172.17.32.1) at 00:12:34:45:67:90 on ue0 permanent [ethernet]
125 return subprocess.check_output(['/usr/sbin/arp', '-n' ,ipaddr], shell=False).split()[3]
126 except subprocess.CalledProcessError:
127 raise MACnotFound
128
129def get_active_MACs():
130 """ Return dictionary with active IPs as keys """
131 # ? (172.17.32.1) at 00:12:34:45:67:90 on ue0 permanent [ethernet]
132 # ? (172.17.32.2) at 00:aa:bb:cc:dd:ee on ue0 expires in 964 seconds [ethernet]
133 # ? (172.16.3.38) at (incomplete) on vr2 expired [ethernet]
134 output = subprocess.check_output(['/usr/sbin/arp', '-n' ,'-a'], shell=False)
135 db = {}
136 for line in output.strip().split('\n'):
137 i = line.split()
138 ip = i[1][1:-1]
139 mac = i[3]
140 db[ip] = mac
141 return db
142
143
144class PacketFilterControl():
145 """ Manage an Packet Filter using pfctl and table wlportal"""
146 def add(self, ipaddr):
147 """ Add Allow Entry in Firewall"""
148 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'add', ipaddr], stderr=subprocess.PIPE).communicate()[1]
149 is_added = '1/1 addresses added.' in output
150 return is_added
151 def delete(self, ipaddr):
152 """ Delete one Allow Entry to Firewall"""
153 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'delete', ipaddr], stderr=subprocess.PIPE).communicate()[1]
154 def flush(self):
155 """ Delete all Allow Entries from Firewall"""
156 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'flush'], stderr=subprocess.PIPE).communicate()[1]
157 #0 addresses deleted.
158 return int(output.strip().split('\n')[-1].split()[0])
159 def cleanup(self, expire_time=None):
160 """ Delete obsolete entries and expired entries from the Firewall"""
161 deleted_entries = 0
162 # Delete entries older than certain time
163 if expire_time:
164 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'expire', expire_time], stdout=subprocess.PIPE).communicate()[0]
165 # 0/0 addresses expired.
166 deleted_entries += int(output.strip.split()[-1].split('/')[0])
167
168 # Delete entries which the MAC<>IP mapping does no longer hold. The
169 # ``rogue'' clients, commonly seen when DHCP scope is small and IPs get
170 # re-used frequently, are wipped and require an re-connect.
171 stored_mac = {}
172 if os.path.isfile(cfg['accessdb']):
173 for line in open(cfg['accessdb'],'r'):
174 (epoch, mac, ipaddr) = line.split()
175 stored_mac[ipaddr] = mac
176 # Live configuration
177 active_mac = get_active_MACs()
178 # Process all active ip addresses from firewall and compare changes
179 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'show'], stdout=subprocess.PIPE).communicate()[0]
180 for ip in output.split():
181 if ip in cfg['whitelist']:
182 # IP is whitelisted
183 continue
184 elif active_mac.has_key(ip) and active_mac[ip] in cfg['whitelist']:
185 # MAC is whitelisted
186 continue
187 elif not active_mac.has_key(ip) and stored_mac.has_key(ip):
188 # In-active connection - Keep entry with normal expire time, as user
189 # might come back (temponary disconnect).
190 continue
191 elif active_mac.has_key(ip) and stored_mac.has_key(ip) and active_mac[ip] == stored_mac[ip]:
192 # Active connection - previous record found - Stored v.s. Active happy
193 continue
194 else:
195 self.delete(ip)
196 deleted_entries =+ 1
197 return deleted_entries
198
199
200# Call from crontab
201if sys.argv[1:]:
202 if sys.argv[1] == 'cleanup':
203 fw = PacketFilterControl()
204 fw.cleanup()
205 sys.exit(0)
206
207### BEGIN STANDALONE/CGI PARSING ###
208#
209# Query String Dictionaries
210qs_post = None
211qs = None
212header = []
213if not os.environ.has_key('REQUEST_METHOD'):
214 # a) We are not wrapped around in a HTTP server, so this _is_ the
215 # HTTP server, so act like one.
216 class TimeoutException(Exception):
217 """ Helper for alarm signal handling"""
218 pass
219
220 def handler(signum, frame):
221 """ Helper for alarm signal handling"""
222 raise TimeoutException
223
224
225 # Parse the HTTP/1.1 Content-Header (partially)
226 signal.signal(signal.SIGALRM,handler)
227 us = None
228 method = None
229 hostname = None
230 content_length = None
231 remote_host = None
232 while True:
233 try:
234 signal.alarm(1)
235 line = sys.stdin.readline().strip()
236 if not line:
237 break
238 header.append(line)
239 signal.alarm(0)
240 if line.startswith('GET '):
241 us = urlparse.urlsplit(line.split()[1])
242 method = 'GET'
243 elif line.startswith('POST '):
244 method = 'POST'
245 us = urlparse.urlsplit(line.split()[1])
246 elif line.startswith('Host: '):
247 hostname = line.split()[1]
248 elif line.startswith('Content-Length: '):
249 content_length = int(line.split()[1])
250 except TimeoutException:
251 break
252
253 # Capture Portal, make sure to redirect all to portal
254 if hostname != cfg['portalroot']:
255 print "HTTP/1.1 302 Moved Temponary\r\n",
256 print "Location: http://%(portalroot)s/\r\n" % cfg,
257 sys.exit(0)
258
259
260 # Handle potential POST
261 if method == 'POST' and content_length:
262 body = sys.stdin.read(content_length)
263 qs_post = urlparse.parse_qs(body)
264
265 # Parse Query String
266 if us and us.path == "/wlportal" and us.query:
267 qs = urlparse.parse_qs(us.query)
268
269 remote_host = os.environ['REMOTEHOST']
270else:
271 # b) CGI Script: Parse the CGI Variables if present
272 if os.environ['REQUEST_METHOD'] == "POST":
273 content_length = int(os.environ['CONTENT_LENGTH'])
274 body = sys.stdin.read(content_length)
275 qs_post = urlparse.parse_qs(body)
276
277 if os.environ.has_key('QUERY_STRING'):
278 qs = urlparse.parse_qs(os.environ['QUERY_STRING'])
279
280 remote_host = os.environ['REMOTE_ADDR']
281#
282### END STANDALONE/CGI PARSING ###
283
284
285# Helpers for HTML 'templates'
286content = cfg.copy()
287content.update(extra_header='')
288
289# IP or MAC on the whitelist does not need to authenticate, used for devices
290# which need to connect to the internet, but has no 'buttons' to press OK.
291#
292# This assumes that devices will re-connect if they are not able to connect
293# to their original host, as we do not preserve the original URI.
294remote_mac = get_mac(remote_host)
295if cfg['autologin'] or remote_host in cfg['whitelist'] or remote_mac in cfg['whitelist']:
296 qs_post = { 'action' : 'login' }
297
298if remote_mac in cfg['warnlist']:
299 connect['status_msg'] = "U veroorzaakt overlast op het WL netwerk || You are causing WL network abuse"
300
301try:
302 fw = PacketFilterControl()
303
304 # Put authenticate use and process response
305 if qs and qs.has_key('action'):
306 if 'flush' in qs['action']:
307 retval = fw.flush()
308 content['status_msg'] += "# [INFO] Deleted %s entries\n" % retval
309 elif 'update' in qs['action']:
310 tech_footer = "# [INFO] Update timestamp of all entries\n"
311 fw.update()
312 content['status_msg'] += fw.get_log()
313 elif 'cleanup' in qs['action']:
314 retval = fw.cleanup(cfg['expire_time'])
315 content['status_msg'] += "# [INFO] Deleted %s entries\n" % retval
316 elif qs_post and qs_post.has_key('action'):
317 if 'login' in qs_post['action']:
318 if remote_mac in cfg['blacklist']:
319 content['status_msg'] = "Toegang ontzegt ipv misbruik WL netwerk || Access denied due to WL network abuse"
320 elif fw.add(remote_host):
321 content['extra_header'] = "Refresh: %(refresh_delay)s; url=%(portal_url)s\r" % content
322 content['status_msg'] = "Sucessfully Logged In! || " +\
323 """ Will redirect you in %(refresh_delay)s seconds to <a href="%(portal_url)s">%(portal_url)s</a> """ % content
324 log_registered_host(remote_mac, remote_host)
325 else:
326 content['status_msg'] = "ERROR! Already Logged On"
327 elif 'logout' in qs_post['action']:
328 fw.delete(remote_host)
329 content['status_msg'] = "Succesfully logged out!"
330
331except Exception, e:
332 content['tech_footer'] += traceback.format_exc()
333 content['status_msg'] = "<div class='error'>Internal error!<pre>%s</pre></div>" % traceback.format_exc()
334 pass
335
336 # Present Main Screen
337print """\
338HTTP/1.1 200 OK\r
339Content-Type: text/html\r
340%(extra_header)s
341""" % content
342
343try:
344 tmpl_file = cfg['tmpl_autologin'] if cfg['autologin'] else cfg['tmpl_login']
345 page = open(tmpl_file,'r').read()
346except IOError:
347 page = """
348<html><head></head><body>
349<h2>%(status_msg)s</h2>
350
351<h3>Wireless Leiden - Internet Portal</h3>
352<form action="http://%(portalroot)s/wlportal/" method="POST">
353<input name="action" type="hidden" value="login" />
354<input type="submit" value="OK, agreed" />
355</form>
356
357<h3>More options</h3>
358<form action="http://%(portalroot)s/wlportal/" method="POST">
359<input name="action" type="hidden" value="logout" />
360<input type="submit" value="Cancel and/or Logout" />
361</form>
362<hr /><em>Technical Details:</em><pre>
363%(tech_footer)s
364</pre>
365</body></html>
366"""
367
368print Template(page).render(content)
Note: See TracBrowser for help on using the repository browser.