87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
import re
|
|
|
|
from flask import Flask, abort, request, redirect
|
|
import nftables
|
|
from pyroute2 import IPDB
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_pyfile('/etc/captive-portal.conf')
|
|
|
|
ipdb = IPDB()
|
|
nft = nftables.Nftables()
|
|
|
|
NFT_TIMEOUT_RE = re.compile(r'((?P<days>[0-9]+)d)?((?P<hours>[0-9]+)h)?((?P<minutes>[0-9]+)m)?((?P<seconds>[0-9]+)s)?((?P<milliseconds>[0-9]+)ms)?')
|
|
def parse_timeout(val):
|
|
m = NFT_TIMEOUT_RE.fullmatch(val)
|
|
if not m:
|
|
return 0
|
|
groups = m.groupdict()
|
|
result = int(groups['days'] or '0') * 24*60*60*1000
|
|
result += int(groups['hours'] or '0') * 60*60*1000
|
|
result += int(groups['minutes'] or '0') * 60*1000
|
|
result += int(groups['seconds'] or '0') * 1000
|
|
result += int(groups['milliseconds'] or '0')
|
|
return result
|
|
|
|
NFT_SET_ELEM_EXPIRES_RE = re.compile(r'elements = \{ (?P<val>[^ ]+) expires (?P<expires>[^ ]+) \}')
|
|
def get_allowed_mac_timeout(mac_addr):
|
|
# As of libnftables 1.0.6 JSON output mode does not support get element
|
|
rc, output, error = nft.cmd(f'get element inet captive_portal allowed_macs {{ {mac_addr} }}')
|
|
if rc != 0:
|
|
return None
|
|
m = NFT_SET_ELEM_EXPIRES_RE.search(output)
|
|
if not m:
|
|
return None
|
|
_mac_addr, expires = m.groups()
|
|
if _mac_addr != mac_addr:
|
|
return None
|
|
return parse_timeout(expires)
|
|
|
|
def add_allowed_mac(mac_addr):
|
|
# "add" does not reset the timeout if mac_addr is already in set
|
|
# "delete" fails if mac_addr is not in set
|
|
# We first make sure mac_addr is in set, then delete, then add so timeout is reset
|
|
rc, output, error = nft.cmd(f'''
|
|
add element inet captive_portal allowed_macs {{ {mac_addr} }}
|
|
delete element inet captive_portal allowed_macs {{ {mac_addr} }}
|
|
add element inet captive_portal allowed_macs {{ {mac_addr} }}
|
|
''')
|
|
return rc == 0
|
|
|
|
def del_allowed_mac(mac_addr):
|
|
rc, output, error = nft.cmd(f'''
|
|
add element inet captive_portal allowed_macs {{ {mac_addr} }}
|
|
delete element inet captive_portal allowed_macs {{ {mac_addr} }}
|
|
''')
|
|
return rc == 0
|
|
|
|
@app.before_request
|
|
def set_remote_mac_addr():
|
|
inet_addr = request.environ.get('HTTP_X_FORWARDED_FOR') or request.remote_addr
|
|
request.remote_mac_addr = ipdb.interfaces[app.config['INTERFACE']].neighbours[inet_addr]['lladdr']
|
|
|
|
@app.route('/api/status')
|
|
def status():
|
|
result = {
|
|
"captive": True,
|
|
"user-portal-url": app.config['USER_PORTAL_URL'],
|
|
}
|
|
mac_timeout = get_allowed_mac_timeout(request.remote_mac_addr)
|
|
if mac_timeout:
|
|
result['captive'] = False
|
|
result['venue-info-url'] = app.config['VENUE_INFO_URL']
|
|
result['seconds-remaining'] = mac_timeout // 1000
|
|
result['can-extend-session'] = True
|
|
return result
|
|
|
|
@app.route('/api/login', methods=['POST'])
|
|
def login():
|
|
if add_allowed_mac(request.remote_mac_addr):
|
|
return redirect(app.config['VENUE_INFO_URL'])
|
|
return redirect(app.config['USER_PORTAL_URL'])
|
|
|
|
@app.route('/api/logout', methods=['POST'])
|
|
def logout():
|
|
del_allowed_mac(request.remote_mac_addr)
|
|
return redirect(app.config['USER_PORTAL_URL'])
|
|
|