vpnunit/main.py

239 lines
7.4 KiB
Python

import sqlite3
import os
from flask import Flask, request, jsonify, render_template, Response
from flask_httpauth import HTTPBasicAuth
import configparser
PREFIX='/data'
if os.environ.get('PREFIX') is not None:
PREFIX=os.environ.get('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>', 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)