1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-22 15:20:09 +01:00

Finally figured out SQLAlchemy and started to re-write some of the APIs. The UI will completely handle by JS with Vue. There will be no more templating from flask to minimize the resource usage ;)

This commit is contained in:
Donald Zou 2024-01-10 01:42:19 -05:00
parent 864f82ba11
commit ba2bcaba07
5 changed files with 191 additions and 20 deletions

View File

@ -1,4 +1,3 @@
from crypt import methods
import sqlite3 import sqlite3
import configparser import configparser
import hashlib import hashlib
@ -13,11 +12,16 @@ import re
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import urllib.error import urllib.error
from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from json import JSONEncoder
from operator import itemgetter from operator import itemgetter
import flask
# PIP installed library # PIP installed library
import ifcfg import ifcfg
from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g
from flask.json.provider import JSONProvider
from flask_qrcode import QRcode from flask_qrcode import QRcode
from icmplib import ping, traceroute from icmplib import ping, traceroute
@ -25,8 +29,8 @@ from icmplib import ping, traceroute
import threading import threading
from sqlalchemy.orm import mapped_column, declarative_base, Session from sqlalchemy.orm import mapped_column, declarative_base, Session
from sqlalchemy import FLOAT, INT, VARCHAR, select, MetaData from sqlalchemy import FLOAT, INT, VARCHAR, select, MetaData, DATETIME
from sqlalchemy import create_engine from sqlalchemy import create_engine, inspect
DASHBOARD_VERSION = 'v3.1' DASHBOARD_VERSION = 'v3.1'
CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.') CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.')
@ -53,6 +57,80 @@ Classes
Base = declarative_base() Base = declarative_base()
class CustomJsonEncoder(JSONProvider):
def dumps(self, obj, **kwargs):
if type(obj) == WireguardConfiguration:
return obj.toJSON()
return json.dumps(obj)
def loads(self, obj, **kwargs):
return json.loads(obj, **kwargs)
app.json = CustomJsonEncoder(app)
class WireguardConfiguration:
__parser: configparser.ConfigParser = configparser.ConfigParser(strict=False)
__parser.optionxform = str
Status: bool = False
Name: str = ""
PrivateKey: str = ""
PublicKey: str = ""
ListenPort: str = ""
Address: str = ""
DNS: str = ""
Table: str = ""
MTU: str = ""
PreUp: str = ""
PostUp: str = ""
PreDown: str = ""
PostDown: str = ""
SaveConfig: bool = False
class InvalidConfigurationFileException(Exception):
def __init__(self, m):
self.message = m
def __str__(self):
return self.message
def __init__(self, name):
self.Name = name
self.__parser.read(os.path.join(WG_CONF_PATH, f'{self.Name}.conf'))
sections = self.__parser.sections()
if "Interface" not in sections:
raise self.InvalidConfigurationFileException(
"[Interface] section not found in " + os.path.join(WG_CONF_PATH, f'{self.Name}.conf'))
interfaceConfig = dict(self.__parser.items("Interface", True))
for i in dir(self):
if str(i) in interfaceConfig.keys():
if isinstance(getattr(self, i), bool):
setattr(self, i, _strToBool(interfaceConfig[i]))
else:
setattr(self, i, interfaceConfig[i])
if self.PrivateKey:
self.PublicKey = self.__getPublicKey()
# Create tables in database
inspector = inspect(engine)
existingTable = inspector.get_table_names()
if self.Name not in existingTable:
_createPeerModel(self.Name).__table__.create(engine)
if self.Name + "_restrict_access" not in existingTable:
_createRestrcitedPeerModel(self.Name).__table__.create(engine)
if self.Name + "_transfer" not in existingTable:
_createPeerTransferModel(self.Name).__table__.create(engine)
def __getPublicKey(self) -> str:
return subprocess.check_output(['wg', 'pubkey'], input=self.PrivateKey.encode()).decode().strip('\n')
def toJSON(self):
return self.__dict__
class DashboardConfig: class DashboardConfig:
def __init__(self): def __init__(self):
@ -113,21 +191,28 @@ class DashboardConfig:
return True, self.__config[section][key] return True, self.__config[section][key]
def ResponseObject(status=True, message=None, data=None) -> dict: def ResponseObject(status=True, message=None, data=None) -> Flask.response_class:
return { response = Flask.make_response(app, {
"status": status, "status": status,
"message": message, "message": message,
"data": data "data": data
} })
response.content_type = "application/json"
return response
DashboardConfig = DashboardConfig() DashboardConfig = DashboardConfig()
WireguardConfigurations: [WireguardConfiguration] = []
''' '''
Private Functions Private Functions
''' '''
def _strToBool(value: str) -> bool:
return value.lower() in ("yes", "true", "t", "1", 1)
def _createPeerModel(wgConfigName): def _createPeerModel(wgConfigName):
class Peer(Base): class Peer(Base):
__tablename__ = wgConfigName __tablename__ = wgConfigName
@ -154,18 +239,62 @@ def _createPeerModel(wgConfigName):
return Peer return Peer
def _createRestrcitedPeerModel(wgConfigName):
class PeerRestricted(Base):
__tablename__ = wgConfigName + "_restrict_access"
id = mapped_column(VARCHAR, primary_key=True)
private_key = mapped_column(VARCHAR)
DNS = mapped_column(VARCHAR)
endpoint_allowed_ip = mapped_column(VARCHAR)
name = mapped_column(VARCHAR)
total_receive = mapped_column(FLOAT)
total_sent = mapped_column(FLOAT)
total_data = mapped_column(FLOAT)
endpoint = mapped_column(VARCHAR)
status = mapped_column(VARCHAR)
latest_handshake = mapped_column(VARCHAR)
allowed_ip = mapped_column(VARCHAR)
cumu_receive = mapped_column(FLOAT)
cumu_sent = mapped_column(FLOAT)
cumu_data = mapped_column(FLOAT)
mtu = mapped_column(INT)
keepalive = mapped_column(INT)
remote_endpoint = mapped_column(VARCHAR)
preshared_key = mapped_column(VARCHAR)
return PeerRestricted
def _createPeerTransferModel(wgConfigName):
class PeerTransfer(Base):
__tablename__ = wgConfigName + "_transfer"
id = mapped_column(VARCHAR, primary_key=True)
total_receive = mapped_column(FLOAT)
total_sent = mapped_column(FLOAT)
total_data = mapped_column(FLOAT)
cumu_receive = mapped_column(FLOAT)
cumu_sent = mapped_column(FLOAT)
cumu_data = mapped_column(FLOAT)
time = mapped_column(DATETIME)
return PeerTransfer
def _regexMatch(regex, text): def _regexMatch(regex, text):
pattern = re.compile(regex) pattern = re.compile(regex)
return pattern.search(text) is not None return pattern.search(text) is not None
def _getConfigurationList(): def _getConfigurationList() -> [WireguardConfiguration]:
conf = [] configurations = []
for i in os.listdir(WG_CONF_PATH): for i in os.listdir(WG_CONF_PATH):
if _regexMatch("^(.{1,}).(conf)$", i): if _regexMatch("^(.{1,}).(conf)$", i):
i = i.replace('.conf', '') i = i.replace('.conf', '')
_createPeerModel(i).__table__.create(engine) try:
_createPeerModel(i + "_restrict_access").__table__.create(engine) configurations.append(WireguardConfiguration(i))
except WireguardConfiguration.InvalidConfigurationFileException as e:
print(f"{i} have an invalid configuration file.")
return configurations
''' '''
@ -173,6 +302,26 @@ API Routes
''' '''
@app.before_request
def auth_req():
authenticationRequired = _strToBool(DashboardConfig.GetConfig("Server", "auth_req")[1])
if authenticationRequired:
if ('/static/' not in request.path and "username" not in session and "/" != request.path
and "validateAuthentication" not in request.path and "authenticate" not in request.path):
resp = Flask.make_response(app, "Not Authorized" + request.path)
resp.status_code = 401
return resp
@app.route('/api/validateAuthentication', methods=["GET"])
def API_ValidateAuthentication():
token = request.cookies.get("authToken") + ""
if token == "" or "username" not in session or session["username"] != token:
return ResponseObject(False, "Invalid authentication")
return ResponseObject(True)
@app.route('/api/authenticate', methods=['POST']) @app.route('/api/authenticate', methods=['POST'])
def API_AuthenticateLogin(): def API_AuthenticateLogin():
data = request.get_json() data = request.get_json()
@ -180,18 +329,31 @@ def API_AuthenticateLogin():
print() print()
if password.hexdigest() == DashboardConfig.GetConfig("Account", "password")[1] \ if password.hexdigest() == DashboardConfig.GetConfig("Account", "password")[1] \
and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]: and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]:
session['username'] = data['username'] authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()
resp = jsonify(ResponseObject(True)) session['username'] = authToken
resp.set_cookie("authToken", resp = ResponseObject(True, "")
hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()) resp.set_cookie("authToken", authToken)
session.permanent = True session.permanent = True
return resp return resp
return jsonify(ResponseObject(False, "Username or password is incorrect.")) return ResponseObject(False, "Username or password is incorrect.")
@app.route('/api/getWireguardConfigurations', methods=["GET"])
def API_getWireguardConfigurations():
pass
@app.route('/api/getDashboardConfiguration', methods=["GET"])
def API_getDashboardConfiguration():
pass
if __name__ == "__main__": if __name__ == "__main__":
engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db')) engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
_, app_ip = DashboardConfig.GetConfig("Server", "app_ip") _, app_ip = DashboardConfig.GetConfig("Server", "app_ip")
_, app_port = DashboardConfig.GetConfig("Server", "app_port") _, app_port = DashboardConfig.GetConfig("Server", "app_port")
_getConfigurationList() _, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
WireguardConfigurations = _getConfigurationList()
app.run(host=app_ip, debug=False, port=app_port) app.run(host=app_ip, debug=False, port=app_port)

View File

@ -3,6 +3,15 @@ import {cookie} from "../utilities/cookie.js";
import Index from "@/views/index.vue" import Index from "@/views/index.vue"
import Signin from "@/views/signin.vue"; import Signin from "@/views/signin.vue";
import ConfigurationList from "@/views/configurationList.vue"; import ConfigurationList from "@/views/configurationList.vue";
import {fetchGet} from "@/utilities/fetch.js";
const checkAuth = async () => {
let result = false
await fetchGet("/api/validateAuthentication", {}, (res) => {
result = res.status
});
return result;
}
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
@ -26,9 +35,9 @@ const router = createRouter({
] ]
}); });
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth){ if (to.meta.requiresAuth){
if (cookie.getCookie("authToken")){ if (cookie.getCookie("authToken") && await checkAuth()){
next() next()
}else{ }else{
next("/signin") next("/signin")

View File

@ -1,6 +1,6 @@
export const fetchGet = async (url, params=undefined, callback=undefined) => { export const fetchGet = async (url, params=undefined, callback=undefined) => {
const urlSearchParams = new URLSearchParams(params); const urlSearchParams = new URLSearchParams(params);
await fetch(`${url}?${urlSearchParams.toString()}}`, { await fetch(`${url}?${urlSearchParams.toString()}`, {
headers: { headers: {
"content-type": "application/json" "content-type": "application/json"
} }

View File

@ -15,7 +15,7 @@ export default defineConfig({
}, },
server:{ server:{
proxy: { proxy: {
'/api': 'http://178.128.231.4:10086/' '/api': 'http://127.0.0.1:10086/'
} }
} }
}) })