import sqlite3 import os from flask import Flask, request, jsonify, render_template, Response from flask_httpauth import HTTPBasicAuth # type: ignore import configparser PREFIX = "/data" if os.environ.get("PREFIX") is not None: PREFIX = os.environ["PREFIX"] app = Flask(__name__) config = configparser.ConfigParser() auth = HTTPBasicAuth() config.read(os.path.join(PREFIX, "vpnunit.config.ini")) class Ex(Exception): def __init__(self, code, message): self._code = code self._message = message def getCode(self): return self._code def __str__(self): return self._message DATABASE = os.path.join(PREFIX, "database.sqlite3") db = sqlite3.connect(DATABASE) cu = db.cursor() db.execute("SELECT name FROM sqlite_master") if len(cu.fetchall()) == 0: print("creating database schema...") db.execute( "CREATE TABLE \ IF NOT EXISTS \ user (id INTEGER PRIMARY KEY, name TEXT UNIQUE)" ) db.execute( "CREATE TABLE \ IF NOT EXISTS \ gateway \ (id INTEGER PRIMARY KEY, \ subid INTEGER NOT NULL, \ name TEXT UNIQUE, \ user INTEGER NOT NULL \ REFERENCES user(id))" ) db.commit() cu.close() db.close() os.environ["EASYRSA_PKI"] = os.path.join(PREFIX, "pki") os.environ["EASYRSA_BATCH"] = "1" os.environ["PATH"] = ( os.environ["PATH"] + ":" + os.getcwd() + "/easy-rsa/easyrsa3" ) @auth.verify_password def verify_user(username, password): if ( username == config["DEFAULT"]["username"] and password == config["DEFAULT"]["password"] ): return username @app.route("/") def hello_world(): return "It works!" @app.route("/users", methods=["GET"]) @auth.login_required def get_users(): db = sqlite3.connect(DATABASE) cu = db.cursor() users = [] for row in cu.execute("SELECT id, name FROM user"): users.append({"id": row[0], "name": row[1]}) cu.close() db.close() return jsonify(users) @app.route("/users", methods=["POST"]) @auth.login_required def post_users(): db = sqlite3.connect(DATABASE) cu = db.cursor() name = request.json["name"] print("creating " + str(name)) try: cu.execute("INSERT INTO user (name) VALUES (?)", (name,)) return jsonify({"status": "ok"}) except sqlite3.Error as e: return jsonify({"status": "error", "message": str(e)}), 409 finally: db.commit() cu.close() db.close() @app.route("/gateways", methods=["GET"]) @auth.login_required def get_gateways(): db = sqlite3.connect(DATABASE) cu = db.cursor() gateways = [] for row in cu.execute( "SELECT g.id, g.subid, g.name, u.name \ FROM gateway AS g \ INNER JOIN user AS u ON u.id = g.user" ): gateways.append( {"id": row[0], "subid": row[1], "name": row[2], "user": row[3]} ) cu.close() db.close() return jsonify(gateways) @app.route("/gateways", methods=["POST"]) @auth.login_required def post_gateways(): db = sqlite3.connect(DATABASE) cu = db.cursor() try: name = request.json["name"] user = request.json["user"] # TODO sanitize name, it must be a FQDN used_gateway_subids = [] for row in cu.execute( "SELECT g.subid, u.id \ FROM user AS u \ LEFT JOIN gateway AS g ON g.user = u.id WHERE u.name = ?", [ str( user, ) ], ): used_gateway_subids.append(row[0]) userid = row[1] # search for an empty id for gateway for subid in range(0, 15): if subid not in used_gateway_subids: break if subid in used_gateway_subids: raise Ex(403, "exit: maximum number of gateways reached for user") cu.execute( "INSERT INTO gateway (subid, name, user) \ VALUES (?, ?, (SELECT id FROM user WHERE name = ?))", [ subid, str( name, ), str( user, ), ], ) os.environ["EASYRSA_REQ_CN"] = name r = os.system("easyrsa gen-req {} nopass".format(name)) if r != 0: raise Ex(500, "exit: {} cannot gen-req".format(r)) r = os.system("easyrsa sign-req client {}".format(name)) if r != 0: raise Ex(500, "exit: {} cannot sign-req".format(r)) ipid = "{:x}{:x}".format(userid, subid) address = "2001:470:c844::" + ipid + "0" network = "2001:470:c844:" + ipid + "0::/60" staticclient = render_template( "staticclient", address=address, network=network ) with open(os.path.join(PREFIX, "ovpn/clients/", name), "w") as f: f.write(staticclient) with open(os.path.join(PREFIX, "ip/routes"), "a") as f: f.write(network + " via " + address + "\n") db.commit() return jsonify({"status": "ok"}) except KeyError as e: return jsonify({"status": "error", "message": str(e)}), 400 except sqlite3.Error as e: return jsonify({"status": "error", "message": str(e)}), 409 except Ex as e: return jsonify({"status": "error", "message": str(e)}), e.getCode() finally: cu.close() db.close() @app.route("/gateway/", methods=["GET"]) @auth.login_required def get_gateway(fqdn): db = sqlite3.connect(DATABASE) cu = db.cursor() gateway = dict() for row in cu.execute( "SELECT g.id, g.subid, g.name, u.name \ FROM gateway AS g \ INNER JOIN user AS u ON u.id = g.user" ): gateway["id"] = row[0] gateway["subid"] = row[1] gateway["name"] = row[2] gateway["user"] = row[3] cu.close() db.close() return jsonify(gateway) @app.route("/gateway//config", methods=["GET"]) @auth.login_required def get_gateway_config(fqdn): # TODO sanity check FQDN # WARNING: maybe you want to do more than a simple sanity check, # or you have to trust the user of these API # eg: he could retrieve the CA.key !!! with open(os.environ["EASYRSA_PKI"] + "/issued/" + fqdn + ".crt") as f: cert = f.read() with open(os.environ["EASYRSA_PKI"] + "/private/" + fqdn + ".key") as f: key = f.read() with open(os.environ["EASYRSA_PKI"] + "/ca.crt") as f: ca = f.read() return Response( render_template("config.ovpn", ca=ca, cert=cert, key=key), mimetype="text/plain", ) @app.route("/gateway//renew", methods=["POST"]) @auth.login_required def post_gateway_renew(fqdn): os.environ["EASYRSA_CERT_EXPIRE"] = "180" # days r = os.system("easyrsa renew {} nopass".format(fqdn)) if r != 0: raise Ex(500, "exit: {} cannot renew") return jsonify({"status": "ok"}) @app.route("/gateway/", methods=["DELETE"]) @auth.login_required def delete_gateway(fqdn): # TODO sanity check for this parameter! Possible system command injection db = sqlite3.connect(DATABASE) cu = db.cursor() for row in cu.execute( "SELECT u.id, g.subid \ FROM gateway AS g \ INNER JOIN user AS u ON u.id = g.user WHERE g.name = ?", [ str( fqdn, ) ], ): userid = row[0] subid = row[1] ipid = "{:x}{:x}".format(userid, subid) break address = "2001:470:c844::" + ipid + "0/64" network = "2001:470:c844:" + ipid + "0::/60" sedrm = ( "sed -i '\\_^" + network + " via " + address + "$_d' " + PREFIX + "/ip/routes" ) print("[sedrm] " + sedrm) r = os.system(sedrm) if r != 0: raise Ex(500, "exit: {} cannot sed-out ip route".format(r)) cu.execute( "DELETE FROM gateway AS g WHERE g.name = ?", [ str( fqdn, ) ], ) try: r = os.system("easyrsa revoke {}".format(fqdn)) if r != 0: raise Ex(500, "exit: {} cannot revoke".format(r)) r = os.system("easyrsa gen-crl") if r != 0: raise Ex(500, "exit: {} cannot gen-crl".format(r)) except Ex as e: return jsonify({"status": "error", "message": str(e)}), e.getCode() os.remove(os.path.join(PREFIX, "ovpn/clients", fqdn)) db.commit() cu.close() db.close() return jsonify({"status": "ok"}) if __name__ == "__main__": app.run(host="::", port=5000, debug=True)