autopeering/web/backend/main.py

554 lines
24 KiB
Python
Raw Normal View History

#! /usr/bin/env python3
from flask import Flask, Response, redirect, render_template, request, session, abort
2022-11-23 16:47:55 +01:00
import werkzeug.exceptions as werkzeug_exceptions
import json
import os
import base64
import logging
import random
from functools import wraps
from ipaddress import ip_address, ip_network, IPv4Network, IPv6Network
import kioubit_verify
app = Flask(__name__)
class Config (dict):
def __init__(self, configfile: str = None):
if configfile:
self.configfile = configfile
else:
if os.path.exists("./config.json"):
self.configfile = "./config.json"
elif os.path.exists("/etc/dn42-autopeer/config.json"):
self.configfile = "/etc/dn42-autopeer/config,json"
else:
raise FileNotFoundError(
"no config file found in ./config.json or /etc/dn42-autopeer/config.json")
2022-11-20 10:57:33 +01:00
self._load_config()
self.keys = self._config.keys
#self.__getitem__ = self._config.__getitem__
super().__init__(self)
2022-11-19 18:12:01 +01:00
def __contains__(self, o):
return self._config.__contains__(o)
def __delitem__(self, v):
raise NotImplementedError()
super().__delitem__(self, v)
def __getitem__(self, k):
return self._config[k]
2022-11-20 10:57:33 +01:00
def _load_config(self):
with open(self.configfile) as cf:
try:
self._config = json.load(cf)
except json.decoder.JSONDecodeError:
raise SyntaxError(f"no valid JSON found in '{cf.name}'")
if not "flask-template-dir" in self._config:
self._config["flask-template-dir"] = "../frontend"
if not "debug-mode" in self._config:
self._config["debug-mode"] = False
if not "base-dir" in self._config:
self._config["base-dir"] = "/"
2022-11-20 10:57:33 +01:00
if not "peerings-data" in self._config:
self._config["peering-data"] = "./peerings"
logging.info(self._config)
class PeeringManager:
2022-11-20 10:57:33 +01:00
def __init__(self, peerings_file):
self._peering_file = peerings_file
2022-11-20 10:57:33 +01:00
self._load_peerings()
def _load_peerings(self):
if not os.path.exists(self._peering_file):
with open(self._peering_file, "x") as p:
json.dump({"mnter": {}, "asn": {}}, p)
try:
with open(self._peering_file, "r") as p:
self.peerings = json.load(p)
except json.decoder.JSONDecodeError:
with open(self._peering_file, "w") as p:
json.dump({"mnter": {}, "asn": {}}, p)
with open(self._peering_file, "r") as p:
self.peerings = json.load(p)
# self.peerings = {}
# missing_peerings = False
# for peering in self._peerings:
# if os.path.exists(f"{self._peering_dir}/{peering}.json"):
# with open(f"{self._peering_dir}/{peering}.json") as peer_cfg:
# self.peerings[peering] = json.load(peer_cfg)
# else:
# logging.warning(f"peering with id {peering} doesn't exist. removing reference in `{self._peering_dir}/peerings.json`")
# self._peerings.remove(peering)
# missing_peerings = True
# if missing_peerings:
# with open(f"{self._peering_dir}/peerings.json","w") as p:
# json.dump(self._peerings, p, indent=4)
def _save_peerings(self):
with open(self._peering_file, "w") as p:
json.dump(self.peerings, p, indent=4)
2022-11-20 10:57:33 +01:00
def _update_nodes(mode, peering, new_peering=None):
"""mode: "add","update","delete
peering: peering to send to node (included in peering)
new_peering: if mode=="update" the new peering to update to
"""
pass
def exists(self, asn, node, mnt=None, wg_key=None, endpoint=None, ipv6ll=None, ipv4=None, ipv6=None, bgp_mp=True, bgp_enh=True):
"""checks if a peerings with specific data already exists"""
# check if mnt is specified, already exists in the database and if that mnt has the specified ASn -> if not: return False
if mnt and not (mnt in self.peerings["mnter"] and asn in self.peerings["mnter"][mnt]):
return False
selected_peerings = self.peerings["asn"][asn]
# check if the ASn even has peerings
if len(selected_peerings) == 0:
return False
for p in selected_peerings:
if p["node"] == node:
if (not wg_key or p["wg_key"] == wg_key) and (not endpoint or p["endpoint"] == endpoint) \
and (not ipv6ll or p["ipv6ll"] == ipv6ll) and (not ipv4 or p["ipv4"] == ipv4) and (not ipv6 or p["ipv6"] == ipv6)\
and (not bgp_mp or p["bgp_mp"] == bgp_mp) and (not bgp_enh or p["bgp_enh"] == bgp_enh):
return True
return False
2022-11-20 10:57:33 +01:00
def get_peerings_by_mnt(self, mnt):
# print(self.peerings)
try:
out = []
for asn in self.peerings["mnter"][mnt]:
try:
for peering in self.peerings["asn"][asn]:
out.append(peering)
except KeyError as e:
pass
return out
except KeyError:
return {}
def add_peering(self, asn, node, mnt, wg_key, endpoint=None, ipv6ll=None, ipv4=None, ipv6=None, bgp_mp=True, bgp_enh=True):
2022-12-03 23:26:04 +01:00
# check if this MNT already has a/this asn
try:
if not asn in self.peerings["mnter"][mnt]:
2022-12-03 23:26:04 +01:00
# ... and add it if it hasn't
self.peerings[mnt].append(asn)
except KeyError:
2022-12-03 23:26:04 +01:00
# ... and cerate it if it doesn't have any yet
self.peerings["mnter"][mnt] = [asn]
try:
if not asn in self.peerings["asn"]:
self.peerings["asn"][asn] = []
except KeyError:
self.peerings["asn"][asn] = []
# deny more than one peering per ASN to one node
for peering in self.peerings["asn"][asn]:
if peering["node"] == node:
return False
self.peerings["asn"][asn].append({"MNT": mnt, "ASN": asn, "node": node, "wg_key": wg_key, "endpoint": endpoint,
"ipv6ll": ipv6ll, "ipv4": ipv4, "ipv6": ipv6, "bgp_mp": bgp_mp, "bgp_enh": bgp_enh})
self._save_peerings()
return True
def update_peering(self, asn, node, mnt, wg_key, endpoint=None, ipv6ll=None, ipv4=None, ipv6=None, bgp_mp=True, bgp_enh=True):
2022-12-03 23:26:04 +01:00
# check if this MNT already has a/this asn
try:
if not asn in self.peerings["mnter"][mnt]:
# ... and add it if it hasn't
self.peerings[mnt].append(asn)
except KeyError:
# ... and cerate it if it doesn't have any yet
self.peerings["mnter"][mnt] = [asn]
try:
if not asn in self.peerings["asn"]:
return False
except KeyError:
return False
2022-12-03 23:26:04 +01:00
success = False
for pNr in range(len(self.peerings["asn"][asn])):
if self.peerings["asn"][asn][pNr]["node"] == node:
self.peerings["asn"][asn][pNr] = {"MNT": mnt, "ASN": asn, "node": node, "wg_key": wg_key,
"endpoint": endpoint, "ipv6ll": ipv6ll, "ipv4": ipv4, "ipv6": ipv6, "bgp_mp": bgp_mp, "bgp_enh": bgp_enh}
2022-12-03 23:26:04 +01:00
success = True
if not success:
return False
2022-12-03 23:26:04 +01:00
self._save_peerings()
return True
def delete_peering(self, asn, node, mnt, wg_key=None):
if not self.exists(asn, node, mnt=mnt, wg_key=wg_key):
return False
for p in self.peerings["asn"][asn]:
if p["node"] == node:
if wg_key and p["wg_key"] != wg_key:
continue
self.peerings["asn"][asn].remove(p)
self._save_peerings()
return True
# if nothing got found (should have been catched by self.exists)
return False
2022-11-20 10:57:33 +01:00
config = Config()
peerings = PeeringManager(config["peerings"])
2022-12-03 13:31:47 +01:00
kverifyer = kioubit_verify.AuthVerifyer(config["domain"])
2022-12-03 13:31:47 +01:00
def check_peering_data(form):
new_peering = {}
# errors = 0
# check if all required (and enabled) options are specified
2022-12-03 13:31:47 +01:00
try:
new_peering["peer-asn"] = session["user-data"]["asn"]
new_peering["peer-wgkey"] = form["peer-wgkey"]
if "peer-endpoint-enabled" in form and form["peer-endpoint-enabled"] == "on":
new_peering["peer-endpoint"] = form["peer-endpoint"]
if new_peering["peer-endpoint"] == "":
raise ValueError("peer-endpoint")
else:
new_peering["peer-endpoint"] = None
2022-12-03 13:31:47 +01:00
if "peer-v6ll-enabled" in form and form["peer-v6ll-enabled"] == "on":
new_peering["peer-v6ll"] = form["peer-v6ll"]
if new_peering["peer-v6ll"] == "":
raise ValueError("peer-v6ll")
else:
new_peering["peer-v6ll"] = None
if "peer-v4-enabled" in form and form["peer-v4-enabled"] == "on":
new_peering["peer-v4"] = form["peer-v4"]
if new_peering["peer-v4"] == "":
raise ValueError("peer-v4")
else:
new_peering["peer-v4"] = None
if "peer-v6-enabled" in form and form["peer-v6-enabled"] == "on":
new_peering["peer-v6"] = form["peer-v6"]
if new_peering["peer-v6"] == "":
raise ValueError("peer-v6")
else:
new_peering["peer-v6"] = None
new_peering["bgp-mp"] = form["bgp-multi-protocol"] if "bgp-multi-protocol" in form else "off"
2022-12-03 23:26:04 +01:00
new_peering["bgp-mp"] = True if new_peering["bgp-mp"] else False
2022-12-03 13:31:47 +01:00
new_peering["bgp-enh"] = form["bgp-extended-next-hop"] if "bgp-extended-next-hop" in form else "off"
2022-12-03 23:26:04 +01:00
new_peering["bgp-enh"] = True if new_peering["bgp-enh"] else False
2022-12-03 13:31:47 +01:00
#new_peering[""] = form["peer-wgkey"]
except ValueError as e:
print(f"error: {e.args}")
return False, "at least one of the required/enabled fields was not filled out"
print(new_peering)
# check wireguard key
2022-12-03 13:31:47 +01:00
wg_key_invalid = False
if len(new_peering["peer-wgkey"]) != 44:
wg_key_invalid = True
try:
base64.b64decode(new_peering["peer-wgkey"])
except:
wg_key_invalid = True
if wg_key_invalid:
2022-12-03 13:31:47 +01:00
return False, "invalid wireguard Key"
# check endpoint
2022-12-03 13:31:47 +01:00
if new_peering["peer-endpoint"]:
if not new_peering["peer-endpoint"].split(":")[-1].isnumeric():
return False, "no port number in endpoint"
elif len(new_peering["peer-endpoint"].split(":")) < 2 and not "." in new_peering["peer-endpoint"]:
return False, "endpoint doesn't look like a ip address or fqdn"
# check if at least one ip is specified/enabled
2022-12-03 13:31:47 +01:00
try:
if not (new_peering["peer-v6ll"] or new_peering["peer-v4"] or new_peering["peer-v6"]):
return False, "at least one of the ip addresses must be enabled and specified"
except KeyError:
return False, "one of the values isn't valid"
# check if supplied ip addresses are valid
2022-12-03 13:31:47 +01:00
try:
if new_peering["peer-v6ll"]:
ipv6ll = ip_address(new_peering["peer-v6ll"])
if not ipv6ll.version == 6:
raise ValueError()
if not ipv6ll.is_link_local:
raise ValueError()
2022-12-03 13:31:47 +01:00
if new_peering["peer-v4"]:
ipv4 = ip_address(new_peering["peer-v4"])
if not ipv4.version == 4:
raise ValueError()
2022-12-03 13:31:47 +01:00
if ipv4.is_link_local:
pass
elif ipv4.is_private:
if not (ipv4.compressed.startswith("172.2") or ipv4.compressed.startswith("10.")):
raise ValueError()
is_in_allowed = False
if session["user-data"]["allowed4"]:
for allowed4 in session["user-data"]["allowed4"].split(","):
if ipv4 in ip_network(allowed4):
is_in_allowed = True
if not is_in_allowed:
return False, "supplied ipv4 addr not in allowed ip range"
else:
raise ValueError()
2022-12-03 13:31:47 +01:00
if new_peering["peer-v6"]:
ipv6 = ip_address(new_peering["peer-v6"])
if not ipv6.version == 6:
raise ValueError()
if not ipv6.is_private:
raise ValueError()
if ipv6.is_link_local:
raise ValueError()
2022-12-03 13:31:47 +01:00
is_in_allowed = False
if session["user-data"]["allowed6"]:
for allowed6 in session["user-data"]["allowed6"].split(","):
if ipv6 in ip_network(allowed6):
is_in_allowed = True
if not is_in_allowed:
return False, "supplied ipv6 addr not in allowed ip range"
except ValueError:
return False, "invalid ip address(es) supplied"
# check bgp options
try:
if new_peering["bgp-mp"] == "off" and new_peering["bgp-enh"] == "on":
return False, "extended next hop requires multiprotocol bgp"
if new_peering["bgp-mp"] == "off":
if not (new_peering["peer-v4"] and (new_peering["peer-v6"] or new_peering["peer-v6ll"])):
return False, "ipv4 and ipv6 addresses required when not having MP-BGP"
except ValueError:
...
return True, new_peering
def auth_required():
def wrapper(f):
@wraps(f)
def decorated(*args, **kwargs):
2022-11-17 06:43:43 +01:00
if not "login" in session:
return redirect(f"{config['base-dir']}login?return={request.url}")
else:
return f(*args, **kwargs)
return decorated
return wrapper
@app.route("/api/auth/kverify", methods=["GET", "POST"])
def kioubit_auth():
try:
2022-11-17 06:43:43 +01:00
params = request.args["params"]
signature = request.args["signature"]
except KeyError:
return render_template("login.html", session=session, config=config, return_addr=session["return_url"], msg='"params" or "signature" missing')
2022-11-17 06:43:43 +01:00
success, msg = kverifyer.verify(params, signature)
try:
logging.debug(base64.b64decode(params))
except:
logging.debug("invalid Base64 data provided")
2022-11-17 06:43:43 +01:00
if success:
session["user-data"] = msg
session["login"] = msg['mnt']
try:
return redirect(session["return_url"])
except KeyError:
return redirect(f"{config['base-dir']}peerings")
2022-11-17 06:43:43 +01:00
else:
try:
return render_template("login.html", session=session, config=config, return_addr=session["return_url"], msg=msg)
except KeyError:
return render_template("login.html", session=session, config=config, return_addr=f"{config['base-dir']}peerings", msg=msg)
2022-11-17 06:43:43 +01:00
@app.route("/logout")
def logout():
session.clear()
return redirect("/")
@app.route("/login", methods=["GET", "POST"])
def login():
2022-11-22 17:16:20 +01:00
print(session)
if request.method == "GET":
2022-11-17 06:43:43 +01:00
session["return_url"] = request.args["return"] if "return" in request.args else ""
2022-11-17 06:43:43 +01:00
return render_template("login.html", session=session, config=config, return_addr=session["return_url"])
elif request.method == "POST" and config["debug-mode"]:
try:
print(request.form)
if request.form["theanswer"] != "42":
msg = "what is the answer for everything?"
return render_template("login.html", session=session, config=config, return_addr=session["return_url"], msg=msg)
mnt = request.form["mnt"]
if not mnt.upper().endswith("-MNT"):
raise ValueError
asn = request.form["asn"]
asn = asn[2:] if asn[:2].lower() == "as" else asn
int(asn)
if "allowed4" in request.form:
allowed4 = request.form["allowed4"]
v4_ranges = allowed4.split(
",") if "," in allowed4 else [allowed4]
for v4_range in v4_ranges:
IPv4Network(v4_range)
else:
allowed4 = None
if "allowed6" in request.form:
allowed6 = request.form["allowed6"]
v6_ranges = allowed6.split(
",") if "," in allowed6 else [allowed6]
for v6_range in v6_ranges:
IPv6Network(v6_range)
else:
allowed6 = None
session["user-data"] = {'asn': asn, 'allowed4': allowed4,
'allowed6': allowed6, 'mnt': mnt, 'authtype': "debug"}
session["login"] = mnt
return redirect(session["return_url"])
except ValueError:
msg = "at least one of the values provided is wrong/invalid"
return render_template("login.html", session=session, config=config, return_addr=session["return_url"], msg=msg)
except KeyError:
msg = "not all required field were specified"
return render_template("login.html", session=session, config=config, return_addr=session["return_url"], msg=msg)
elif request.method == "POST" and not config["debug-mode"]:
abort(405)
return redirect(request.args["return"])
@app.route("/peerings/delete", methods=["GET", "POST", "DELETE"])
@auth_required()
def peerings_delete():
if request.method == "GET":
return render_template("peerings-delete.html", session=session, config=config, request_args=request.args)
return f"{request.method} /peerings/delete?{str(request.args)}{str(request.form)}"
elif request.method in ["POST", "DELETE"]:
if not request.form["confirm"] == "on":
return render_template("peerings-delete.html", session=session, config=config, request_args=request.args, msg="you have to confirm the deletion first")
if not peerings.exists(request.args["asn"], request.args["node"], mnt=session["login"]):
return render_template("peerings-delete.html", session=session, config=config, request_args=request.args, msg="the peering you requested to delete doesn't exist (anymore) or you are not authorized to delete it")
print(str(request))
if not peerings.delete_peering(request.args["asn"], request.args["node"], mnt=session["login"]):
return render_template("peerings-delete.html", session=session, config=config, request_args=request.args, msg="deletion of the peering requested failed, maybe you are not authorized or that peering doesn't exist")
session["msg"] = {"msg": "peer-del",
"node": request.args["node"], "asn": request.args["asn"]}
return redirect("../peerings")
return f"{request.method} /peerings/delete {request.args} {request.form}"
@app.route("/peerings/edit", methods=["GET", "POST"])
@auth_required()
def peerings_edit():
print(session)
if request.method == "GET":
2022-12-03 23:26:04 +01:00
mnt_peerings = peerings.get_peerings_by_mnt(session["login"])
# print(mnt_peerings)
if "node" in request.args and request.args["node"] in config["nodes"]:
2022-12-03 23:26:04 +01:00
selected_peering = None
for p in mnt_peerings:
if p["node"] == request.args["node"] and p["ASN"] == request.args["asn"]:
selected_peering = p
print(p)
break
return render_template("peerings-edit.html", session=session, config=config, mnt_peerings=mnt_peerings, selected_peering=selected_peering)
else:
2022-12-03 23:26:04 +01:00
return render_template("peerings-edit.html", session=session, config=config, mnt_peerings=mnt_peerings, selected_peering=None)
elif request.method == "POST":
2022-12-03 23:26:04 +01:00
print(request.args)
print(request.form)
if not "node" in request.args or not request.args["node"]:
return render_template("peerings-edit.html", session=session, config=config, peerings=peerings, msg="no node specified, please click one of the buttons above")
2022-12-03 23:26:04 +01:00
peering_valid, peering_or_msg = check_peering_data(request.form)
print(peering_valid)
print(peering_or_msg)
if not peering_valid:
return render_template("peerings-edit.html", session=session, config=config, peerings=peerings, msg=peering_or_msg), 400
2022-12-03 23:26:04 +01:00
if not peerings.update_peering(session["user-data"]["mnt"], session["user-data"]["asn"], request.args["node"], peering_or_msg["peer-wgkey"], peering_or_msg["peer-endpoint"], peering_or_msg["peer-v6ll"], peering_or_msg["peer-v4"], peering_or_msg["peer-v6"], peering_or_msg["bgp-mp"], peering_or_msg["bgp-enh"]):
return render_template("peerings-edit.html", session=session, config=config, peerings=peerings, msg="such a peering doesn't exist(yet)"), 400
2022-12-03 23:26:04 +01:00
return redirect(f"{config['base-dir']}peerings")
return f"{request.method} /peerings/edit?{str(request.args)}{str(request.form)}"
@app.route("/peerings/new", methods=["GET", "POST"])
@auth_required()
def peerings_new():
2022-11-22 17:16:20 +01:00
print(session)
if request.method == "GET":
if "node" in request.args and request.args["node"] in config["nodes"]:
return render_template("peerings-new.html", config=config, selected_node=request.args["node"], peerings=peerings)
else:
return render_template("peerings-new.html", session=session, config=config, peerings=peerings)
elif request.method == "POST":
2022-11-23 16:47:55 +01:00
print(request.args)
print(request.form)
if not "node" in request.args or not request.args["node"]:
return render_template("peerings-new.html", session=session, config=config, peerings=peerings, msg="no node specified, please click one of the buttons above")
2022-11-23 16:47:55 +01:00
2022-12-03 13:31:47 +01:00
peering_valid, peering_or_msg = check_peering_data(request.form)
2022-11-23 16:47:55 +01:00
2022-12-03 13:31:47 +01:00
if not peering_valid:
return render_template("peerings-new.html", session=session, config=config, peerings=peerings, msg=peering_or_msg), 400
2022-12-03 23:26:04 +01:00
if not peerings.add_peering(session["user-data"]["mnt"], session["user-data"]["asn"], request.args["node"], peering_or_msg["peer-wgkey"], peering_or_msg["peer-endpoint"], peering_or_msg["peer-v6ll"], peering_or_msg["peer-v4"], peering_or_msg["peer-v6"], peering_or_msg["bgp-mp"], peering_or_msg["bgp-enh"]):
return render_template("peerings-new.html", session=session, config=config, peerings=peerings, msg="this ASN already has a peering with the requested node"), 400
2022-11-23 16:47:55 +01:00
return redirect(f"{config['base-dir']}peerings")
2022-12-03 23:26:04 +01:00
@app.route("/peerings", methods=["GET", "POST", "DELETE"])
@auth_required()
def peerings_view():
2022-11-22 17:16:20 +01:00
print(session)
2022-11-17 06:43:43 +01:00
if request.method == "GET":
if "node" in request.args and request.args["node"] in config["nodes"]:
return render_template("peerings.html", config=config, selected_node=request.args["node"], peerings=peerings)
else:
return render_template("peerings.html", session=session, config=config, peerings=peerings)
2022-11-17 06:43:43 +01:00
elif request.method == "POST":
return peerings_new()
elif request.method == "DELETE":
return peerings_delete()
2022-11-17 06:43:43 +01:00
else:
abort(405)
2022-11-17 06:43:43 +01:00
@app.route("/")
def index():
2022-11-22 17:16:20 +01:00
print(session)
# print(config._config["nodes"])
# for node in config["nodes"].values():
# print (node)
2022-11-17 06:43:43 +01:00
return render_template("index.html", session=session, config=config._config)
def main():
app.static_folder = config["flask-template-dir"]+"/static/"
app.template_folder = config["flask-template-dir"]
app.secret_key = config["flask-secret-key"]
2022-11-19 18:12:01 +01:00
if "production" in config and config["production"] == False:
logging.getLogger(__name__).setLevel(0)
app.run(host=config["listen"], port=config["port"],
debug=config["debug-mode"], threaded=True)
2022-11-19 18:12:01 +01:00
else:
from waitress import serve
logging.getLogger(__name__).setLevel(logging.INFO)
2022-11-19 18:12:01 +01:00
serve(app, host=config["listen"], port=config["port"])
if __name__ == "__main__":
main()