diff --git a/src/dashboard_new.py b/src/dashboard_new.py index 171ea5b..fadf2f5 100644 --- a/src/dashboard_new.py +++ b/src/dashboard_new.py @@ -18,9 +18,11 @@ from json import JSONEncoder from operator import itemgetter from typing import Dict, Any +import bcrypt import flask # PIP installed library import ifcfg +import pyotp from flask import Flask, request, render_template, redirect, url_for, session, jsonify, g from flask.json.provider import JSONProvider from flask_qrcode import QRcode @@ -138,15 +140,24 @@ class WireguardConfiguration: return self.__dict__ +def iPv46RegexCheck(ip): + return re.match( + '((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))', + ip) + + class DashboardConfig: def __init__(self): self.__config = configparser.ConfigParser(strict=False) self.__config.read(DASHBOARD_CONF) + self.hiddenAttribute = ["totp_key"] self.__default = { "Account": { "username": "admin", - "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" + "password": "admin", + "enable_totp": "false", + "totp_key": pyotp.random_base32() }, "Server": { "wg_conf_path": "/etc/wireguard", @@ -156,7 +167,7 @@ class DashboardConfig: "version": DASHBOARD_VERSION, "dashboard_refresh_interval": "60000", "dashboard_sort": "status", - "dashboard_theme": "light" + "dashboard_theme": "dark" }, "Peers": { "peer_global_DNS": "1.1.1.1", @@ -165,6 +176,9 @@ class DashboardConfig: "remote_endpoint": ifcfg.default_interface()['inet'], "peer_MTU": "1420", "peer_keep_alive": "21" + }, + "Other": { + "welcome_session": "true" } } @@ -172,13 +186,71 @@ class DashboardConfig: for key, value in keys.items(): exist, currentData = self.GetConfig(section, key) if not exist: - self.SetConfig(section, key, value) + self.SetConfig(section, key, value, True) + + def __configValidation(self, key, value: Any) -> [bool, str]: + if type(value) is str and len(value) == 0: + return False, "Field cannot be empty!" + if key == "peer_global_dns": + value = value.split(",") + for i in value: + try: + ipaddress.ip_address(i) + except ValueError as e: + return False, str(e) + if key == "peer_endpoint_allowed_ip": + value = value.split(",") + for i in value: + try: + ipaddress.ip_network(i, strict=False) + except Exception as e: + return False, str(e) + if key == "wg_conf_path": + if not os.path.exists(value): + return False, f"{value} is not a valid path" + if key == "password": + if self.GetConfig("Account", "password")[0]: + if not self.__checkPassword( + value["currentPassword"], self.GetConfig("Account", "password")[1].encode("utf-8")): + return False, "Current password does not match." + if value["newPassword"] != value["repeatNewPassword"]: + return False, "New passwords does not match" + return True, "" + + def generatePassword(self, plainTextPassword: str): + return bcrypt.hashpw(plainTextPassword.encode("utf-8"), bcrypt.gensalt(rounds=12)) + + def __checkPassword(self, plainTextPassword: str, hashedPassword: bytes): + return bcrypt.checkpw(plainTextPassword.encode("utf-8"), hashedPassword) + + def SetConfig(self, section: str, key: str, value: any, init: bool = False) -> [bool, str]: + if key in self.hiddenAttribute and not init: + return False, None + + if not init: + valid, msg = self.__configValidation(key, value) + if not valid: + return False, msg + + if section == "Account" and key == "password": + if not init: + value = self.generatePassword(value["newPassword"]).decode("utf-8") + else: + value = self.generatePassword(value).decode("utf-8") - def SetConfig(self, section: str, key: str, value: any) -> bool: if section not in self.__config: self.__config[section] = {} - self.__config[section][key] = value - return self.SaveConfig() + + if key not in self.__config[section].keys() or value != self.__config[section][key]: + if type(value) is bool: + if value: + self.__config[section][key] = "true" + else: + self.__config[section][key] = "false" + else: + self.__config[section][key] = value + return self.SaveConfig(), "" + return True, "" def SaveConfig(self) -> bool: try: @@ -195,14 +267,27 @@ class DashboardConfig: if key not in self.__config[section]: return False, None + if self.__config[section][key] in ["1", "yes", "true", "on"]: + return True, True + + if self.__config[section][key] in ["0", "no", "false", "off"]: + return True, False + return True, self.__config[section][key] def toJSON(self) -> dict[str, dict[Any, Any]]: the_dict = {} + for section in self.__config.sections(): the_dict[section] = {} for key, val in self.__config.items(section): - the_dict[section][key] = val + if key not in self.hiddenAttribute: + if val in ["1", "yes", "true", "on"]: + the_dict[section][key] = True + elif val in ["0", "no", "false", "off"]: + the_dict[section][key] = False + else: + the_dict[section][key] = val return the_dict @@ -319,10 +404,14 @@ API Routes @app.before_request def auth_req(): - authenticationRequired = _strToBool(DashboardConfig.GetConfig("Server", "auth_req")[1]) + authenticationRequired = 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): + and "validateAuthentication" not in request.path and "authenticate" not in request.path + and "getDashboardConfiguration" not in request.path and "getDashboardTheme" not in request.path + and "isTotpEnabled" not in request.path + ): resp = Flask.make_response(app, "Not Authorized" + request.path) resp.status_code = 401 return resp @@ -340,17 +429,32 @@ def API_ValidateAuthentication(): @app.route('/api/authenticate', methods=['POST']) def API_AuthenticateLogin(): data = request.get_json() - password = hashlib.sha256(data['password'].encode()) - print() - if password.hexdigest() == DashboardConfig.GetConfig("Account", "password")[1] \ - and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]: + valid = bcrypt.checkpw(data['password'].encode("utf-8"), + DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8")) + totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1] + totpValid = False + + if totpEnabled: + totpValid = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp'] + + if (valid + and data['username'] == DashboardConfig.GetConfig("Account", "username")[1] + and ((totpEnabled and totpValid) or not totpEnabled) + ): authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest() session['username'] = authToken - resp = ResponseObject(True, "") + resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1]) resp.set_cookie("authToken", authToken) session.permanent = True return resp - return ResponseObject(False, "Username or password is incorrect.") + return ResponseObject(False, "Username, password or OTP is incorrect.") + + +@app.route('/api/signout') +def API_SignOut(): + resp = ResponseObject(True, "") + resp.delete_cookie("authToken") + return resp @app.route('/api/getWireguardConfigurations', methods=["GET"]) @@ -364,6 +468,95 @@ def API_getDashboardConfiguration(): return ResponseObject(data=DashboardConfig.toJSON()) +@app.route('/api/updateDashboardConfiguration', methods=["POST"]) +def API_updateDashboardConfiguration(): + data = request.get_json() + for section in data['DashboardConfiguration'].keys(): + for key in data['DashboardConfiguration'][section].keys(): + if not DashboardConfig.SetConfig(section, key, data['DashboardConfiguration'][section][key])[0]: + return ResponseObject(False, "Section or value is invalid.") + return ResponseObject() + + +@app.route('/api/updateDashboardConfigurationItem', methods=["POST"]) +def API_updateDashboardConfigurationItem(): + data = request.get_json() + if "section" not in data.keys() or "key" not in data.keys() or "value" not in data.keys(): + return ResponseObject(False, "Invalid request.") + + valid, msg = DashboardConfig.SetConfig( + data["section"], data["key"], data['value']) + + if not valid: + return ResponseObject(False, msg) + + return ResponseObject() + + +@app.route('/api/getDashboardTheme') +def API_getDashboardTheme(): + return ResponseObject(data=DashboardConfig.GetConfig("Server", "dashboard_theme")[1]) + + +@app.route('/api/isTotpEnabled') +def API_isTotpEnabled(): + return ResponseObject(data=DashboardConfig.GetConfig("Account", "enable_totp")[1]) + + +@app.route('/api/Welcome_GetTotpLink') +def API_Welcome_GetTotpLink(): + if DashboardConfig.GetConfig("Other", "welcome_session")[1]: + return ResponseObject( + data=pyotp.totp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).provisioning_uri( + issuer_name="WGDashboard")) + return ResponseObject(False) + + +@app.route('/api/Welcome_VerifyTotpLink', methods=["POST"]) +def API_Welcome_VerifyTotpLink(): + data = request.get_json() + if DashboardConfig.GetConfig("Other", "welcome_session")[1]: + return ResponseObject(pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp']) + return ResponseObject(False) + + +@app.route('/api/Welcome_Finish', methods=["POST"]) +def API_Welcome_Finish(): + data = request.get_json() + if DashboardConfig.GetConfig("Other", "welcome_session")[1]: + if data["username"] == "": + return ResponseObject(False, "Username cannot be blank.") + + if data["newPassword"] == "" or len(data["newPassword"]) < 8: + return ResponseObject(False, "Password must be at least 8 characters") + + updateUsername, updateUsernameErr = DashboardConfig.SetConfig("Account", "username", data["username"]) + updatePassword, updatePasswordErr = DashboardConfig.SetConfig("Account", "password", + { + "newPassword": data["newPassword"], + "repeatNewPassword": data[ + "repeatNewPassword"], + "currentPassword": "admin" + }) + updateEnableTotp, updateEnableTotpErr = DashboardConfig.SetConfig("Account", "enable_totp", data["enable_totp"]) + + if not updateUsername or not updatePassword or not updateEnableTotp: + return ResponseObject(False, f"{updateUsernameErr},{updatePasswordErr},{updateEnableTotpErr}".strip(",")) + + DashboardConfig.SetConfig("Other", "welcome_session", False) + + return ResponseObject() + + +@app.route('/', methods=['GET']) +def index(): + """ + Index page related + @return: Template + """ + return render_template('index_new.html') + + if __name__ == "__main__": engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db')) @@ -371,5 +564,4 @@ if __name__ == "__main__": _, app_port = DashboardConfig.GetConfig("Server", "app_port") _, 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=True, port=app_port) diff --git a/src/static/app/index.html b/src/static/app/index.html index 8e026c5..4f1d70c 100644 --- a/src/static/app/index.html +++ b/src/static/app/index.html @@ -7,7 +7,7 @@