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[0-9]+)d)?((?P[0-9]+)h)?((?P[0-9]+)m)?((?P[0-9]+)s)?((?P[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[^ ]+) expires (?P[^ ]+) \}') 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'])