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

Kind of finished revamping add peers

Still need to clean some of the codes but overall is good :)
This commit is contained in:
Donald Zou 2024-05-12 00:39:17 +08:00
parent 57c2e89f00
commit 769ca4e26d
8 changed files with 243 additions and 131 deletions

7
src/bulkAddBash.sh Normal file
View File

@ -0,0 +1,7 @@
for ((i = 0 ; i<$1 ; i++ ))
do
privateKey=$(wg genkey)
presharedKey=$(wg genkey)
publicKey=$(wg pubkey <<< "$privateKey")
echo "$privateKey,$publicKey,$presharedKey"
done

View File

@ -1240,7 +1240,7 @@ def add_peer(config_name):
return "Please fill in all required box."
if not isinstance(keys, list):
return config_name + " is not running."
if public_key in keys:
if public_key in keys:d;lp
return "Public key already exist."
check_dup_ip = g.cur.execute(
"SELECT COUNT(*) FROM " + config_name + " WHERE allowed_ip LIKE '" + allowed_ips + "/%'", ) \

View File

@ -28,7 +28,6 @@ import psutil
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
from json import JSONEncoder
from icmplib import ping, traceroute
@ -41,7 +40,7 @@ from sqlalchemy import FLOAT, INT, VARCHAR, select, MetaData, DATETIME
from sqlalchemy import create_engine, inspect
from flask.json.provider import DefaultJSONProvider
DASHBOARD_VERSION = 'v3.1'
DASHBOARD_VERSION = 'v4.0'
CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.')
DB_PATH = os.path.join(CONFIGURATION_PATH, 'db')
if not os.path.isdir(DB_PATH):
@ -57,8 +56,6 @@ UPDATE = None
app = Flask("WGDashboard")
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 5206928
app.secret_key = secrets.token_urlsafe(32)
# Enable QR Code Generator
QRcode(app)
class ModelEncoder(JSONEncoder):
@ -74,7 +71,14 @@ Classes
'''
# Base = declarative_base(class_registry=dict())
def ResponseObject(status=True, message=None, data=None) -> Flask.response_class:
response = Flask.make_response(app, {
"status": status,
"message": message,
"data": data
})
response.content_type = "application/json"
return response
class CustomJsonEncoder(DefaultJSONProvider):
@ -90,38 +94,6 @@ class CustomJsonEncoder(DefaultJSONProvider):
app.json = CustomJsonEncoder(app)
class Peer:
def __init__(self, tableData):
# for i in range(0, len(table)):
# tableData[table.description[i][0]] = table[i]
self.id = tableData["id"]
self.private_key = tableData["private_key"]
self.DNS = tableData["DNS"]
self.endpoint_allowed_ip = tableData["endpoint_allowed_ip"]
self.name = tableData["name"]
self.total_receive = tableData["total_receive"]
self.total_sent = tableData["total_sent"]
self.total_data = tableData["total_data"]
self.endpoint = tableData["endpoint"]
self.status = tableData["status"]
self.latest_handshake = tableData["latest_handshake"]
self.allowed_ip = tableData["allowed_ip"]
self.cumu_receive = tableData["cumu_receive"]
self.cumu_sent = tableData["cumu_sent"]
self.cumu_data = tableData["cumu_data"]
self.mtu = tableData["mtu"]
self.keepalive = tableData["keepalive"]
self.remote_endpoint = tableData["remote_endpoint"]
self.preshared_key = tableData["preshared_key"]
def toJson(self):
return self.__dict__
def __repr__(self):
return str(self.toJson())
class WireguardConfiguration:
class InvalidConfigurationFileException(Exception):
def __init__(self, m):
@ -200,7 +172,7 @@ class WireguardConfiguration:
# Create tables in database
self.__createDatabase()
self.__getPeers()
self.getPeersList()
def __createDatabase(self):
existingTables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
@ -326,12 +298,12 @@ class WireguardConfiguration:
""" % self.Name
, newPeer)
sqldb.commit()
self.Peers.append(Peer(newPeer))
self.Peers.append(Peer(newPeer, self))
else:
cursor.execute("UPDATE %s SET allowed_ip = ? WHERE id = ?" % self.Name,
(i.get("AllowedIPs", "N/A"), i['PublicKey'],))
sqldb.commit()
self.Peers.append(Peer(checkIfExist))
self.Peers.append(Peer(checkIfExist, self))
except ValueError:
pass
@ -462,7 +434,7 @@ class WireguardConfiguration:
self.getStatus()
return True, None
def getPeers(self):
def getPeersList(self):
self.__getPeers()
return self.Peers
@ -483,6 +455,94 @@ class WireguardConfiguration:
}
class Peer:
def __init__(self, tableData, configuration: WireguardConfiguration):
self.configuration = configuration
self.id = tableData["id"]
self.private_key = tableData["private_key"]
self.DNS = tableData["DNS"]
self.endpoint_allowed_ip = tableData["endpoint_allowed_ip"]
self.name = tableData["name"]
self.total_receive = tableData["total_receive"]
self.total_sent = tableData["total_sent"]
self.total_data = tableData["total_data"]
self.endpoint = tableData["endpoint"]
self.status = tableData["status"]
self.latest_handshake = tableData["latest_handshake"]
self.allowed_ip = tableData["allowed_ip"]
self.cumu_receive = tableData["cumu_receive"]
self.cumu_sent = tableData["cumu_sent"]
self.cumu_data = tableData["cumu_data"]
self.mtu = tableData["mtu"]
self.keepalive = tableData["keepalive"]
self.remote_endpoint = tableData["remote_endpoint"]
self.preshared_key = tableData["preshared_key"]
def toJson(self):
return self.__dict__
def __repr__(self):
return str(self.toJson())
def updatePeer(self, name: str, private_key: str,
preshared_key: str,
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
keepalive: int) -> ResponseObject:
existingAllowedIps = [item for row in list(
map(lambda x: [q.strip() for q in x.split(',')],
map(lambda y: y.allowed_ip,
list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row]
if allowed_ip in existingAllowedIps:
return ResponseObject(False, "Allowed IP already taken by another peer.")
if not _checkIPWithRange(endpoint_allowed_ip):
return ResponseObject(False, f"Endpoint Allowed IPs format is incorrect.")
if len(dns_addresses) > 0 and not _checkDNS(dns_addresses):
return ResponseObject(False, f"DNS format is incorrect.")
if mtu < 0 or mtu > 1460:
return ResponseObject(False, "MTU format is not correct.")
if keepalive < 0:
return ResponseObject(False, "Persistent Keepalive format is not correct.")
if len(private_key) > 0:
pubKey = _generatePrivateKey(private_key)
if not pubKey[0] or pubKey[1] != self.id:
return ResponseObject(False, "Private key does not match with the public key.")
try:
if len(preshared_key) > 0:
rd = random.Random()
uid = uuid.UUID(int=rd.getrandbits(128), version=4)
with open(f"{uid}", "w+") as f:
f.write(preshared_key)
updatePsk = subprocess.check_output(
f"wg set {self.configuration.Name} peer {self.id} preshared-key {uid}",
shell=True, stderr=subprocess.STDOUT)
os.remove(str(uid))
if len(updatePsk.decode().strip("\n")) != 0:
return ResponseObject(False,
"Update peer failed when updating preshared key")
updateAllowedIp = subprocess.check_output(
f'wg set {self.configuration.Name} peer {self.id} allowed-ips "{allowed_ip.replace(" ", "")}"',
shell=True, stderr=subprocess.STDOUT)
if len(updateAllowedIp.decode().strip("\n")) != 0:
return ResponseObject(False,
"Update peer failed when updating allowed IPs")
saveConfig = subprocess.check_output(f"wg-quick save {self.configuration.Name}",
shell=True, stderr=subprocess.STDOUT)
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
return ResponseObject(False,
"Update peer failed when saving the configuration.")
cursor.execute(
'''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
keepalive = ?, preshared_key = ? WHERE id = ?''' % self.configuration.Name,
(name, private_key, dns_addresses, endpoint_allowed_ip, mtu,
keepalive, preshared_key, self.id,)
)
return ResponseObject()
except subprocess.CalledProcessError as exc:
return ResponseObject(False, exc.output.decode("UTF-8").strip())
# Regex Match
def regex_match(regex, text):
pattern = re.compile(regex)
@ -498,8 +558,10 @@ def iPv46RegexCheck(ip):
class DashboardConfig:
def __init__(self):
if not os.path.exists(DASHBOARD_CONF):
open(DASHBOARD_CONF, "x")
self.__config = configparser.ConfigParser(strict=False)
self.__config.read_file(open(DASHBOARD_CONF, "w+"))
self.__config.read_file(open(DASHBOARD_CONF, "r+"))
self.hiddenAttribute = ["totp_key"]
self.__default = {
"Account": {
@ -640,18 +702,8 @@ class DashboardConfig:
return the_dict
def ResponseObject(status=True, message=None, data=None) -> Flask.response_class:
response = Flask.make_response(app, {
"status": status,
"message": message,
"data": data
})
response.content_type = "application/json"
return response
DashboardConfig = DashboardConfig()
WireguardConfigurations: {str: WireguardConfiguration} = {}
WireguardConfigurations: dict[str, WireguardConfiguration] = {}
'''
Private Functions
@ -729,7 +781,19 @@ def _generatePublicKey(privateKey) -> [bool, str]:
return False, None
def _getWireguardConfigurationAvailableIP(configName) -> [bool, list[str]]:
def _generatePrivateKey() -> [bool, str]:
try:
publicKey = subprocess.check_output(f"wg genkey", shell=True,
stderr=subprocess.STDOUT)
return True, publicKey.decode().strip('\n')
except subprocess.CalledProcessError:
return False, None
def _getWireguardConfigurationAvailableIP(configName: str) -> tuple[bool, list[str]] | tuple[bool, None]:
if configName not in WireguardConfigurations.keys():
return False, None
configuration = WireguardConfigurations[configName]
@ -921,7 +985,6 @@ def API_updateDashboardConfigurationItem():
def API_updatePeerSettings(configName):
data = request.get_json()
id = data['id']
if len(id) > 0 and configName in WireguardConfigurations.keys():
name = data['name']
private_key = data['private_key']
@ -929,69 +992,80 @@ def API_updatePeerSettings(configName):
allowed_ip = data['allowed_ip']
endpoint_allowed_ip = data['endpoint_allowed_ip']
preshared_key = data['preshared_key']
mtu = data['mtu']
keepalive = data['keepalive']
wireguardConfig = WireguardConfigurations[configName]
foundPeer, peer = wireguardConfig.searchPeer(id)
if foundPeer:
for p in wireguardConfig.Peers:
if allowed_ip in p.allowed_ip and p.id != peer.id:
return ResponseObject(False, f"Allowed IP already taken by another peer.")
if not _checkIPWithRange(endpoint_allowed_ip):
return ResponseObject(False, f"Endpoint Allowed IPs format is incorrect.")
if len(dns_addresses) > 0 and _checkDNS(dns_addresses):
return ResponseObject(False, f"DNS format is incorrect.")
if data['mtu'] < 0 or data['mtu'] > 1460:
return ResponseObject(False, "MTU format is not correct.")
if data['keepalive'] < 0:
return ResponseObject(False, "Persistent Keepalive format is not correct.")
if len(private_key) > 0:
pubKey = _generatePublicKey(private_key)
if not pubKey[0] or pubKey[1] != peer.id:
return ResponseObject(False, "Private key does not match with the public key.")
try:
rd = random.Random()
uid = uuid.UUID(int=rd.getrandbits(128), version=4)
with open(f"{uid}", "w+") as f:
f.write(preshared_key)
updatePsk = subprocess.check_output(
f"wg set {configName} peer {peer.id} preshared-key {uid}",
shell=True, stderr=subprocess.STDOUT)
os.remove(str(uid))
if len(updatePsk.decode().strip("\n")) != 0:
return ResponseObject(False,
"Update peer failed when updating preshared key: " + updatePsk.decode().strip(
"\n"))
allowed_ip = allowed_ip.replace(" ", "")
updateAllowedIp = subprocess.check_output(
f'wg set {configName} peer {peer.id} allowed-ips "{allowed_ip}"',
shell=True, stderr=subprocess.STDOUT)
if len(updateAllowedIp.decode().strip("\n")) != 0:
return ResponseObject(False,
"Update peer failed when updating allowed IPs: " + updateAllowedIp.decode().strip(
"\n"))
saveConfig = subprocess.check_output(f"wg-quick save {configName}",
shell=True, stderr=subprocess.STDOUT)
if f"wg showconf {configName}" not in saveConfig.decode().strip('\n'):
return ResponseObject(False,
"Update peer failed when saving the configuration." + saveConfig.decode().strip(
'\n'))
cursor.execute(
'''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
keepalive = ?, preshared_key = ? WHERE id = ?''' % configName,
(name, private_key, dns_addresses, endpoint_allowed_ip, data["mtu"],
data["keepalive"], preshared_key, id,)
)
return ResponseObject()
except subprocess.CalledProcessError as exc:
return ResponseObject(False, exc.output.decode("UTF-8").strip())
return peer.updatePeer(name, private_key, preshared_key, dns_addresses,
allowed_ip, endpoint_allowed_ip, mtu, keepalive)
return ResponseObject(False, "Peer does not exist")
@app.route('/api/addPeers/<configName>', methods=['POST'])
def API_addPeers(configName):
data = request.get_json()
bulkAdd = data['bulkAdd']
bulkAddAmount = data['bulkAddAmount']
public_key = data['public_key']
allowed_ips = data['allowed_ips']
endpoint_allowed_ip = data['endpoint_allowed_ip']
dns_addresses = data['DNS']
mtu = data['mtu']
keep_alive = data['keepalive']
preshared_key = data['preshared_key']
if configName in WireguardConfigurations.keys():
config = WireguardConfigurations.get(configName)
if (not bulkAdd and (len(public_key) == 0 or len(allowed_ips) == 0)) or len(endpoint_allowed_ip) == 0:
return ResponseObject(False, "Please fill in all required box.")
if not config.getStatus():
return ResponseObject(False,
f"{configName} is not running, please turn on the configuration before adding peers.")
if bulkAdd:
if bulkAddAmount < 1:
return ResponseObject(False, "Please specify amount of peers you want to add")
availableIps = _getWireguardConfigurationAvailableIP(configName)
if not availableIps[0]:
return ResponseObject(False, "No more available IP can assign")
if bulkAddAmount > len(availableIps[1]):
return ResponseObject(False,
f"The maximum number of peers can add is {len(availableIps[1])}")
keyPairs = []
for i in range(bulkAddAmount):
key = _generatePrivateKey()[1]
keyPairs.append([key, _generatePublicKey(key), _generatePrivateKey()[1]])
if len(keyPairs) == 0:
return ResponseObject(False, "Generating key pairs by bulk failed")
print(keyPairs)
else:
if config.searchPeer(public_key)[0] is True:
return ResponseObject(False, f"This peer already exist.")
name = data['name']
private_key = data['private_key']
subprocess.check_output(
f"wg set {config.Name} peer {public_key} allowed-ips {''.join(allowed_ips)}",
shell=True, stderr=subprocess.STDOUT)
if len(preshared_key) > 0:
subprocess.check_output(
f"wg set {config.Name} peer {public_key} preshared-key {preshared_key}",
shell=True, stderr=subprocess.STDOUT)
subprocess.check_output(
f"wg-quick save {config.Name}", shell=True, stderr=subprocess.STDOUT)
config.getPeersList()
found, peer = config.searchPeer(public_key)
if found:
return peer.updatePeer(name, private_key, preshared_key, dns_addresses, ",".join(allowed_ips),
endpoint_allowed_ip, mtu, keep_alive)
return ResponseObject(False, "Configuration does not exist")
@app.route("/api/downloadPeer/<configName>")
def API_downloadPeer(configName):
data = request.args
@ -1050,7 +1124,7 @@ def API_getConfigurationInfo():
return ResponseObject(False, "Please provide configuration name")
return ResponseObject(data={
"configurationInfo": WireguardConfigurations[configurationName],
"configurationPeers": WireguardConfigurations[configurationName].getPeers()
"configurationPeers": WireguardConfigurations[configurationName].getPeersList()
})
@ -1082,7 +1156,9 @@ def API_Welcome_GetTotpLink():
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'])
totp = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now()
print(totp)
return ResponseObject(totp == data['totp'])
return ResponseObject(False)

View File

@ -1,7 +1,7 @@
Flask
bcrypt
ifcfg
psutil
pyotp
flask
icmplib
flask-qrcode
gunicorn
certbot
sqlalchemy

View File

@ -29,14 +29,14 @@ export default {
searchAvailableIps(){
return this.availableIpSearchString ?
this.availableIp.filter(x =>
x.includes(this.availableIpSearchString) && !this.data.allowed_ip.includes(x)) :
this.availableIp.filter(x => !this.data.allowed_ip.includes(x))
x.includes(this.availableIpSearchString) && !this.data.allowed_ips.includes(x)) :
this.availableIp.filter(x => !this.data.allowed_ips.includes(x))
}
},
methods: {
addAllowedIp(ip){
if(this.store.checkCIDR(ip)){
this.data.allowed_ip.push(ip);
this.data.allowed_ips.push(ip);
return true;
}
return false;
@ -55,11 +55,11 @@ export default {
<label for="peer_allowed_ip_textbox" class="form-label">
<small class="text-muted">Allowed IPs <code>(Required)</code></small>
</label>
<div class="d-flex gap-2 flex-wrap" :class="{'mb-2': this.data.allowed_ip.length > 0}">
<div class="d-flex gap-2 flex-wrap" :class="{'mb-2': this.data.allowed_ips.length > 0}">
<TransitionGroup name="list">
<span class="badge rounded-pill text-bg-success" v-for="(ip, index) in this.data.allowed_ip" :key="ip">
<span class="badge rounded-pill text-bg-success" v-for="(ip, index) in this.data.allowed_ips" :key="ip">
{{ip}}
<a role="button" @click="this.data.allowed_ip.splice(index, 1)">
<a role="button" @click="this.data.allowed_ips.splice(index, 1)">
<i class="bi bi-x-circle-fill ms-1"></i></a>
</span>
</TransitionGroup>

View File

@ -1,6 +1,5 @@
<script>
// import {Popover, Dropdown} from "bootstrap";
import {fetchGet} from "@/utilities/fetch.js";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import NameInput from "@/components/configurationComponents/newPeersComponents/nameInput.vue";
import PrivatePublicKeyInput from "@/components/configurationComponents/newPeersComponents/privatePublicKeyInput.vue";
@ -27,7 +26,7 @@ export default {
bulkAdd: false,
bulkAddAmount: "",
name: "",
allowed_ip: [],
allowed_ips: [],
private_key: "",
public_key: "",
DNS: this.dashboardStore.Configuration.Peers.peer_global_dns,
@ -54,6 +53,19 @@ export default {
const dashboardStore = DashboardConfigurationStore();
return {store, dashboardStore}
},
methods: {
peerCreate(){
fetchPost("/api/addPeers/" + this.$route.params.id, this.data, (res) => {
if (res.status){
this.$router.push(`/configuration/${this.$route.params.id}/peers`)
this.dashboardStore.newMessage("Server", "Peer create successfully", "success")
}else{
this.dashboardStore.newMessage("Server", res.message, "danger")
}
})
}
},
computed:{
allRequireFieldsFilled(){
let status = true;
@ -63,8 +75,7 @@ export default {
}
}else{
let requireFields =
["allowed_ip", "private_key", "public_key", "endpoint_allowed_ip", "keepalive", "mtu"]
["allowed_ips", "private_key", "public_key", "endpoint_allowed_ip", "keepalive", "mtu"]
requireFields.forEach(x => {
if (this.data[x].length === 0) status = false;
});
@ -121,6 +132,7 @@ export default {
<div class="d-flex mt-2">
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
:disabled="!this.allRequireFieldsFilled"
@click="this.peerCreate()"
>
<i class="bi bi-plus-circle-fill me-2"></i>Add
</button>

View File

@ -0,0 +1,17 @@
import subprocess
def _generateKeyPairs(amount: int) -> list[list[str]] | None:
try:
pairs = subprocess.check_output(
f'''for ((i = 0 ; i<{amount} ; i++ ));do privateKey=$(wg genkey) presharedKey=$(wg genkey) publicKey=$(wg pubkey <<< "$privateKey") echo "$privateKey,$publicKey,$presharedKey"; done''', shell=True, stderr=subprocess.STDOUT
)
pairs = pairs.decode().split("\n")
print(pairs)
return [x.split(",") for x in pairs]
except subprocess.CalledProcessError as exp:
print(str(exp))
return []
_generateKeyPairs(20)

View File

@ -71,7 +71,7 @@ export default {
{{this.errorMessage}}
</div>
<div class="d-flex flex-column gap-3">
<div id="createAccount">
<div id="createAccount" class="d-flex flex-column gap-2">
<div class="form-group text-body">
<label for="username" class="mb-1 text-muted">
<small>Pick an username you like</small></label>