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

Last change on this file since 10795 was 10795, checked in by rick, 13 years ago

[1:-1] is voor het IP address en niet voor het mac adress. Zorg ervoor dat er
complete goede mac addressen opgeslagen worden.

Related-To: nodefactory:ticket:161

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 11.7 KB
Line 
1#!/usr/bin/env python
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 10795 2012-05-12 16:38:21Z rick $
30# Author : Rick van der Zwet <info@rickvanderzwet.nl>
31# Licence : BSDLike http://wirelessleiden.nl/LICENSE
32import cgitb
33cgitb.enable(logdir="/tmp")
34
35
36import os
37import signal
38import subprocess
39import socket
40import sys
41import time
42import traceback
43import urlparse
44import yaml
45
46from jinja2 import Template
47
48# XXX: Make me dynamic for example put me in the conf file
49cfg = {
50 'autologin' : False,
51 'cmd_arp' : '/usr/sbin/arp',
52 'pfctl' : '/sbin/pfctl',
53 'portal_sponsor': 'Sponsor van Stichting Wireless Leiden',
54 'portal_url' : 'http://wirelessleiden.nl/welkom?connected_to=%s' % socket.gethostname(),
55 'portalroot' : '172.31.255.1',
56 'refresh_delay' : 3,
57 'tmpl_autologin': '/usr/local/etc/wlportal/autologin.tmpl',
58 'tmpl_login' : '/usr/local/etc/wlportal/login.tmpl',
59 'whitelist' : [],
60 'config_files' : ['/usr/local/etc/wlportal/config.yaml','/etc/wleiden.yaml'],
61 'expire_time' : None,
62 'accessdb' : '/var/db/clients',
63 'net_status' : '/tmp/network.status',
64}
65
66
67# No failback if config does not exist, to really make sure the user knows if
68# the config file failed to parse properly or is non-existing
69for config_file in cfg['config_files']:
70 if os.path.isfile(config_file):
71 cfg.update(yaml.load(open(config_file)))
72
73internet_up = True
74if os.path.isfile(cfg['net_status']):
75 internet_up = 'internet=up' in open(cfg['net_status'], 'r').read().lower()
76
77if not internet_up:
78 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"
79
80def log_registered_host(remote_mac, remote_host):
81 """
82 Write statistics file, used for (nagios) monitoring purposes
83 """
84 with open(cfg['accessdb'],"a") as fh:
85 epoch = int(time.time())
86 fh.write("%s %s %s \n" % (epoch, remote_mac, remote_host) )
87
88class MACnotFound(Exception):
89 pass
90
91def get_mac(ipaddr):
92 """ Find out the MAC for a certain IP address """
93 try:
94 # ? (172.17.32.1) at 00:12:34:45:67:90 on ue0 permanent [ethernet]
95 return subprocess.check_output(['/usr/sbin/arp', '-n' ,ipaddr], shell=False).split()[3]
96 except subprocess.CalledProcessError:
97 raise MACnotFound
98
99def get_active_MACs():
100 """ Return dictionary with active IPs as keys """
101 # ? (172.17.32.1) at 00:12:34:45:67:90 on ue0 permanent [ethernet]
102 # ? (172.17.32.2) at 00:aa:bb:cc:dd:ee on ue0 expires in 964 seconds [ethernet]
103 # ? (172.16.3.38) at (incomplete) on vr2 expired [ethernet]
104 output = subprocess.check_output(['/usr/sbin/arp', '-n' ,'-a'], shell=False)
105 db = {}
106 for line in output.strip().split('\n'):
107 i = line.split()
108 mac = i[3]
109 ip = i[5][1:-1]
110 db[ip] = mac
111 return db
112
113
114class PacketFilterControl():
115 """ Manage an Packet Filter using pfctl and table wlportal"""
116 def add(self, ipaddr):
117 """ Add Allow Entry in Firewall"""
118 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'add', ipaddr], stderr=subprocess.PIPE).communicate()[1]
119 is_added = '1/1 addresses added.' in output
120 return is_added
121 def delete(self, ipaddr):
122 """ Delete one Allow Entry to Firewall"""
123 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'delete', ipaddr], stderr=subprocess.PIPE).communicate()[1]
124 def flush(self):
125 """ Delete all Allow Entries from Firewall"""
126 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'flush'], stderr=subprocess.PIPE).communicate()[1]
127 #0 addresses deleted.
128 return int(output.strip().split('\n')[-1].split()[0])
129 def cleanup(self, expire_time=None):
130 """ Delete obsolete entries and expired entries from the Firewall"""
131 deleted_entries = 0
132 # Delete entries older than certain time
133 if expire_time:
134 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'expire', expire_time], stdout=subprocess.PIPE).communicate()[0]
135 # 0/0 addresses expired.
136 deleted_entries += int(output.strip.split()[-1].split('/')[0])
137
138 # Delete entries which the MAC<>IP mapping does no longer hold. The
139 # ``rogue'' clients, commonly seen when DHCP scope is small and IPs get
140 # re-used frequently, are wipped and require an re-connect.
141 stored_mac = {}
142 if os.path.isfile(cfg['accessdb']):
143 for line in open(cfg['accessdb'],'r'):
144 (epoch, mac, ipaddr) = line.split()
145 stored_mac[ipaddr] = mac
146 # Live configuration
147 active_mac = get_active_MACs()
148 # Process all active ip addresses from firewall and compare changes
149 output = subprocess.Popen([cfg['pfctl'],'-t','wlportal', '-T', 'show'], stdout=subprocess.PIPE).communicate()[0]
150 for ip in output.split():
151 if ip in cfg['whitelist']:
152 # IP is whitelisted
153 continue
154 elif active_mac.has_key(ip) and active_mac[ip] in cfg['whitelist']:
155 # MAC is whitelisted
156 continue
157 elif not active_mac.has_key(ip) and stored_mac.has_key(ip):
158 # In-active connection - Keep entry with normal expire time, as user
159 # might come back (temponary disconnect).
160 continue
161 elif active_mac.has_key(ip) and stored_mac.has_key(ip) and active_mac[ip] == stored_mac[ip]:
162 # Active connection - previous record found - Stored v.s. Active happy
163 continue
164 else:
165 self.delete(ip)
166 deleted_entries =+ 1
167 return deleted_entries
168
169
170# Call from crontab
171if sys.argv[1:]:
172 if sys.argv[1] == 'cleanup':
173 fw = PacketFilterControl()
174 fw.cleanup()
175 sys.exit(0)
176
177### BEGIN STANDALONE/CGI PARSING ###
178#
179# Query String Dictionaries
180qs_post = None
181qs = None
182header = []
183if not os.environ.has_key('REQUEST_METHOD'):
184 # a) We are not wrapped around in a HTTP server, so this _is_ the
185 # HTTP server, so act like one.
186 class TimeoutException(Exception):
187 """ Helper for alarm signal handling"""
188 pass
189
190 def handler(signum, frame):
191 """ Helper for alarm signal handling"""
192 raise TimeoutException
193
194
195 # Parse the HTTP/1.1 Content-Header (partially)
196 signal.signal(signal.SIGALRM,handler)
197 us = None
198 method = None
199 hostname = None
200 content_length = None
201 remote_host = None
202 while True:
203 try:
204 signal.alarm(1)
205 line = sys.stdin.readline().strip()
206 if not line:
207 break
208 header.append(line)
209 signal.alarm(0)
210 if line.startswith('GET '):
211 us = urlparse.urlsplit(line.split()[1])
212 method = 'GET'
213 elif line.startswith('POST '):
214 method = 'POST'
215 us = urlparse.urlsplit(line.split()[1])
216 elif line.startswith('Host: '):
217 hostname = line.split()[1]
218 elif line.startswith('Content-Length: '):
219 content_length = int(line.split()[1])
220 except TimeoutException:
221 break
222
223 # Capture Portal, make sure to redirect all to portal
224 if hostname != cfg['portalroot']:
225 print "HTTP/1.1 302 Moved Temponary\r\n",
226 print "Location: http://%(portalroot)s/\r\n" % cfg,
227 sys.exit(0)
228
229
230 # Handle potential POST
231 if method == 'POST' and content_length:
232 body = sys.stdin.read(content_length)
233 qs_post = urlparse.parse_qs(body)
234
235 # Parse Query String
236 if us and us.path == "/wlportal" and us.query:
237 qs = urlparse.parse_qs(us.query)
238
239 remote_host = os.environ['REMOTEHOST']
240else:
241 # b) CGI Script: Parse the CGI Variables if present
242 if os.environ['REQUEST_METHOD'] == "POST":
243 content_length = int(os.environ['CONTENT_LENGTH'])
244 body = sys.stdin.read(content_length)
245 qs_post = urlparse.parse_qs(body)
246
247 if os.environ.has_key('QUERY_STRING'):
248 qs = urlparse.parse_qs(os.environ['QUERY_STRING'])
249
250 remote_host = os.environ['REMOTE_ADDR']
251#
252### END STANDALONE/CGI PARSING ###
253
254
255# Helpers for HTML 'templates'
256content = cfg.copy()
257content.update(extra_header='')
258
259# IP or MAC on the whitelist does not need to authenticate, used for devices
260# which need to connect to the internet, but has no 'buttons' to press OK.
261#
262# This assumes that devices will re-connect if they are not able to connect
263# to their original host, as we do not preserve the original URI.
264remote_mac = get_mac(remote_host)
265if cfg['autologin'] or remote_host in cfg['whitelist'] or remote_mac in cfg['whitelist']:
266 qs_post = { 'action' : 'login' }
267
268try:
269 fw = PacketFilterControl()
270
271 # Put authenticate use and process response
272 if qs and qs.has_key('action'):
273 if 'flush' in qs['action']:
274 retval = fw.flush()
275 content['status_msg'] += "# [INFO] Deleted %s entries\n" % retval
276 elif 'update' in qs['action']:
277 tech_footer = "# [INFO] Update timestamp of all entries\n"
278 fw.update()
279 content['status_msg'] += fw.get_log()
280 elif 'cleanup' in qs['action']:
281 retval = fw.cleanup(cfg['expire_time'])
282 content['status_msg'] += "# [INFO] Deleted %s entries\n" % retval
283 elif qs_post and qs_post.has_key('action'):
284 if 'login' in qs_post['action']:
285 if fw.add(remote_host):
286 content['extra_header'] = "Refresh: %(refresh_delay)s; url=%(portal_url)s\r" % content
287 content['status_msg'] = "Sucessfully Logged In! || " +\
288 """ Will redirect you in %(refresh_delay)s seconds to <a href="%(portal_url)s">%(portal_url)s</a> """ % content
289 log_registered_host(remote_mac, remote_host)
290 else:
291 content['status_msg'] = "ERROR! Already Logged On"
292 elif 'logout' in qs_post['action']:
293 fw.delete(remote_host)
294 content['status_msg'] = "Succesfully logged out!"
295
296except Exception, e:
297 content['tech_footer'] += traceback.format_exc()
298 content['status_msg'] = "<div class='error'>Internal error!<pre>%s</pre></div>" % traceback.format_exc()
299 pass
300
301 # Present Main Screen
302print """\
303HTTP/1.1 200 OK\r
304Content-Type: text/html\r
305%(extra_header)s
306""" % content
307
308try:
309 tmpl_file = cfg['tmpl_autologin'] if cfg['autologin'] else cfg['tmpl_login']
310 page = open(tmpl_file,'r').read()
311except IOError:
312 page = """
313<html><head></head><body>
314<h2>%(status_msg)s</h2>
315
316<h3>Wireless Leiden - Internet Portal</h3>
317<form action="http://%(portalroot)s/wlportal/" method="POST">
318<input name="action" type="hidden" value="login" />
319<input type="submit" value="OK, agreed" />
320</form>
321
322<h3>More options</h3>
323<form action="http://%(portalroot)s/wlportal/" method="POST">
324<input name="action" type="hidden" value="logout" />
325<input type="submit" value="Cancel and/or Logout" />
326</form>
327<hr /><em>Technical Details:</em><pre>
328%(tech_footer)s
329</pre>
330</body></html>
331"""
332
333print Template(page).render(content)
Note: See TracBrowser for help on using the repository browser.