340 lines
8.5 KiB
Python
340 lines
8.5 KiB
Python
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/<fqdn>", 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/<fqdn>/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/<fqdn>/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/<fqdn>", 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)
|