Development
This commit is contained in:
parent
19f0bfa79c
commit
687e0a66d1
8 changed files with 142 additions and 1 deletions
|
|
@ -18,7 +18,7 @@ def collect_tokens(cfg):
|
||||||
'Set Restricted VLAN = Captive Portal on the Network Layout page.'
|
'Set Restricted VLAN = Captive Portal on the Network Layout page.'
|
||||||
)
|
)
|
||||||
|
|
||||||
tokens['CAPTIVE_HTTP_PORT'] = str(cp.get('http_port', 8081))
|
tokens['CAPTIVE_HTTP_PORT'] = str(cp.get('http_port', 25328))
|
||||||
tokens['CAPTIVE_HTTPS_DOMAIN'] = cp.get('https_domain', '')
|
tokens['CAPTIVE_HTTPS_DOMAIN'] = cp.get('https_domain', '')
|
||||||
|
|
||||||
display_rows = []
|
display_rows = []
|
||||||
|
|
|
||||||
6
docker/routlin-portal/Dockerfile
Normal file
6
docker/routlin-portal/Dockerfile
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY app/ .
|
||||||
|
CMD ["python", "main.py"]
|
||||||
23
docker/routlin-portal/app/config_utils.py
Normal file
23
docker/routlin-portal/app/config_utils.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import copy, json, os
|
||||||
|
|
||||||
|
CONFIGS_DIR = '/routlin_location'
|
||||||
|
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
||||||
|
CAPTIVE_QUEUE = f'{CONFIGS_DIR}/.captive-queue'
|
||||||
|
|
||||||
|
_config_cache = None
|
||||||
|
_config_mtime = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
global _config_cache, _config_mtime
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(CONFIG_FILE)
|
||||||
|
if _config_cache is not None and mtime == _config_mtime:
|
||||||
|
return copy.deepcopy(_config_cache)
|
||||||
|
with open(CONFIG_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
_config_cache = data
|
||||||
|
_config_mtime = mtime
|
||||||
|
return copy.deepcopy(data)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
10
docker/routlin-portal/app/main.py
Normal file
10
docker/routlin-portal/app/main.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from flask import Flask
|
||||||
|
from pages.portal.page import bp as portal_bp
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.register_blueprint(portal_bp)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import config_utils
|
||||||
|
port = config_utils.load_config().get('captive_portal', {}).get('http_port', 25328)
|
||||||
|
app.run(host='0.0.0.0', port=port)
|
||||||
0
docker/routlin-portal/app/pages/portal/__init__.py
Normal file
0
docker/routlin-portal/app/pages/portal/__init__.py
Normal file
89
docker/routlin-portal/app/pages/portal/page.py
Normal file
89
docker/routlin-portal/app/pages/portal/page.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import ipaddress
|
||||||
|
from flask import Blueprint, request, redirect
|
||||||
|
import config_utils
|
||||||
|
|
||||||
|
bp = Blueprint('portal', __name__)
|
||||||
|
|
||||||
|
PORTAL_HTML = """\
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{title}</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: sans-serif; max-width: 480px; margin: 60px auto; padding: 0 1rem; }}
|
||||||
|
h1 {{ font-size: 1.5rem; margin-bottom: .5rem; }}
|
||||||
|
.err {{ color: #c00; margin: .75rem 0; }}
|
||||||
|
.terms label {{ display: block; margin: .4rem 0; cursor: pointer; }}
|
||||||
|
button {{ margin-top: 1rem; padding: .55rem 1.4rem; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
{splash_html}
|
||||||
|
{error_html}
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="next" value="{next_url}">
|
||||||
|
<div class="terms">{terms_html}</div>
|
||||||
|
<button type="submit">Continue</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def _vlan_for_ip(client_ip):
|
||||||
|
cfg = config_utils.load_config()
|
||||||
|
try:
|
||||||
|
addr = ipaddress.ip_address(client_ip)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
for vlan in cfg.get('vlans', []):
|
||||||
|
if vlan.get('restricted_vlan') != 'c':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
net = ipaddress.ip_network(f"{vlan['ip']}/{vlan['subnet']}", strict=False)
|
||||||
|
if addr in net:
|
||||||
|
return vlan
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _render(vlan, error=None, next_url=''):
|
||||||
|
terms = vlan.get('portal_terms', [])
|
||||||
|
terms_html = ''.join(
|
||||||
|
f'<label><input type="checkbox" name="term_{i}" required> {t}</label>'
|
||||||
|
for i, t in enumerate(terms)
|
||||||
|
) or '<p>No terms required.</p>'
|
||||||
|
return PORTAL_HTML.format(
|
||||||
|
title=vlan.get('portal_splash_title', 'Guest Portal'),
|
||||||
|
splash_html=f'<p>{vlan["portal_splash_text"]}</p>' if vlan.get('portal_splash_text') else '',
|
||||||
|
error_html=f'<p class="err">{error}</p>' if error else '',
|
||||||
|
terms_html=terms_html,
|
||||||
|
next_url=next_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/', defaults={'path': ''}, methods=['GET', 'POST'])
|
||||||
|
@bp.route('/<path:path>', methods=['GET', 'POST'])
|
||||||
|
def portal(path):
|
||||||
|
vlan = _vlan_for_ip(request.remote_addr)
|
||||||
|
if vlan is None:
|
||||||
|
return 'Portal unavailable.', 404
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
terms = vlan.get('portal_terms', [])
|
||||||
|
for i in range(len(terms)):
|
||||||
|
if not request.form.get(f'term_{i}'):
|
||||||
|
return _render(vlan,
|
||||||
|
error='You must accept all terms to continue.',
|
||||||
|
next_url=request.form.get('next', '')), 200
|
||||||
|
try:
|
||||||
|
with open(config_utils.CAPTIVE_QUEUE, 'a') as f:
|
||||||
|
f.write(f'allow {request.remote_addr}\n')
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return redirect(request.form.get('next') or 'http://routlin.local/', 302)
|
||||||
|
|
||||||
|
return _render(vlan, next_url=request.args.get('next', '')), 200
|
||||||
12
docker/routlin-portal/docker-compose.yml
Normal file
12
docker/routlin-portal/docker-compose.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
name: routlin-portal
|
||||||
|
|
||||||
|
services:
|
||||||
|
portal:
|
||||||
|
container_name: routlin-portal
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "25328:25328"
|
||||||
|
volumes:
|
||||||
|
- $HOME/routlin/config.json:/routlin_location/config.json:ro
|
||||||
|
- $HOME/routlin/.captive-queue:/routlin_location/.captive-queue
|
||||||
|
restart: unless-stopped
|
||||||
1
docker/routlin-portal/requirements.txt
Normal file
1
docker/routlin-portal/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
flask
|
||||||
Loading…
Add table
Add a link
Reference in a new issue