improved Database & Models
This commit is contained in:
commit
0bbe91bec3
18 changed files with 956 additions and 0 deletions
15
lib/generator/wg_config.mjs
Normal file
15
lib/generator/wg_config.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import {
|
||||
instance,
|
||||
peer,
|
||||
address
|
||||
} from "../models.mjs";
|
||||
|
||||
|
||||
export async function generateWireguardServerConfig(interfaceName) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
export async function generateWireguardClientConfig(interfaceName) {
|
||||
|
||||
}
|
||||
465
lib/models.mjs
Normal file
465
lib/models.mjs
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
import {
|
||||
object,
|
||||
array,
|
||||
string,
|
||||
number,
|
||||
enums,
|
||||
boolean,
|
||||
literal,
|
||||
optional,
|
||||
dynamic,
|
||||
nullable,
|
||||
defaulted,
|
||||
union,
|
||||
size,
|
||||
create,
|
||||
assert,
|
||||
define,
|
||||
} from "superstruct";
|
||||
import { Database } from "all.db";
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
ipv4,
|
||||
ipv6
|
||||
} from "cidr-block";
|
||||
import path from "node:path";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
|
||||
class ObjectModel {
|
||||
#modelName;
|
||||
#modelStruct;
|
||||
#modelStore;
|
||||
#dynRef = {};
|
||||
#hooks = new EventEmitter();
|
||||
|
||||
constructor(settings) {
|
||||
this.#modelName = settings.name;
|
||||
this.#modelStruct = settings.model;
|
||||
this.#modelStore = new Database({
|
||||
dataPath: path.join(process.cwd(), 'data/database', `${settings.name}.json`)
|
||||
});
|
||||
|
||||
if (settings.hooks) {
|
||||
Object.entries(settings.hooks).forEach(([eventName, eventFunction]) => {
|
||||
this.on(eventName, eventFunction);
|
||||
})
|
||||
}
|
||||
|
||||
this.#dynRef.refOne = dynamic((objectId, context) => {
|
||||
let permittedObjIds = Object.values(this.getAll()).map(item => item.id);
|
||||
return enums(permittedObjIds);
|
||||
});
|
||||
|
||||
this.#dynRef.refMany = dynamic((objectId, context) => {
|
||||
let permittedObjIds = Object.values(this.getAll()).map(item => item.id);
|
||||
return array(
|
||||
enums(permittedObjIds)
|
||||
);
|
||||
});
|
||||
|
||||
if (settings.dynRef instanceof Object) {
|
||||
this.#dynRef = Object.assign(this.#dynRef, settings.dynRef)
|
||||
}
|
||||
}
|
||||
|
||||
complete(data) {
|
||||
try {
|
||||
let result = create(data, this.#modelStruct);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`error completing ${this.#modelName}: ` + error.message);
|
||||
}
|
||||
}
|
||||
validate(data) {
|
||||
try {
|
||||
assert(data, this.#modelStruct);
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error(`error validating ${this.#modelName}: ` + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.#modelStore.getAll();
|
||||
}
|
||||
getById(objectId) {
|
||||
return this.#modelStore.get(objectId);
|
||||
}
|
||||
create(objectData) {
|
||||
// try {
|
||||
const newData = this.validate(
|
||||
this.complete(objectData)
|
||||
);
|
||||
|
||||
if (this.#modelStore.exists(newData.id)) {
|
||||
throw new Error(`failed create ${this.#modelName} with ID ${newData.id}. already existing in database.`);
|
||||
}
|
||||
|
||||
this.#hooks.emit('beforeCreate', null, newData);
|
||||
this.#modelStore.set(newData.id, newData);
|
||||
this.#hooks.emit('afterCreate', newData);
|
||||
return newData;
|
||||
// } catch (error) {
|
||||
// throw error;
|
||||
// }
|
||||
}
|
||||
update(objectData) {
|
||||
// try {
|
||||
const newData = this.validate(
|
||||
this.complete(objectData)
|
||||
);
|
||||
const oldData = this.getById(newData.id)
|
||||
|
||||
if (!oldData) {
|
||||
throw new Error(`failed update ${this.#modelName} with ID ${newData.id}. not existing in database.`);
|
||||
}
|
||||
|
||||
this.#hooks.emit('beforeUpdate', oldData, newData);
|
||||
this.#modelStore.set(newData.id, newData);
|
||||
this.#hooks.emit('afterUpdate', oldData, newData);
|
||||
return newData;
|
||||
// } catch (error) {
|
||||
// throw error;
|
||||
// }
|
||||
}
|
||||
delete(objectId) {
|
||||
// try {
|
||||
const oldData = this.getById(objectId);
|
||||
|
||||
if (!oldData) {
|
||||
throw new Error(`failed delete ${this.#modelName} with ID ${newData.id}. not existing in database.`);
|
||||
}
|
||||
this.#hooks.emit('beforeDelete', oldData, null);
|
||||
this.#modelStore.delete(oldData.id);
|
||||
this.#hooks.emit('afterDelete', oldData, null);
|
||||
return oldData;
|
||||
// } catch (error) {
|
||||
// throw error;
|
||||
// }
|
||||
}
|
||||
has(objectId) {
|
||||
return this.#modelStore.has(objectId);
|
||||
}
|
||||
get dynRef() {
|
||||
return this.#dynRef;
|
||||
}
|
||||
|
||||
on(eventName, eventFunction) {
|
||||
this.#hooks.on(eventName, eventFunction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
//
|
||||
// Custom Checks
|
||||
//
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
|
||||
const ipv4Address = define('ipv4Address', (value) => {
|
||||
return ipv4.isValidAddress(value);
|
||||
});
|
||||
|
||||
const ipv4Cidr = define('ipv4Cidr', (value) => {
|
||||
console.log(value);
|
||||
console.log(ipv4.isValidCIDR(value));
|
||||
|
||||
return ipv4.isValidCIDR(value);
|
||||
});
|
||||
|
||||
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
//
|
||||
// Base Models
|
||||
//
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
|
||||
// Authorisation Provider Model
|
||||
export const authProvider = new ObjectModel({
|
||||
name: 'authProvider',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: string(),
|
||||
login_text: string(),
|
||||
type: enums(["oauth2"]),
|
||||
settings: dynamic((value, context) => {
|
||||
switch (value.type) {
|
||||
case "oauth2":
|
||||
return object({
|
||||
client_id: string(),
|
||||
client_secret: string(),
|
||||
authorize_url: string(),
|
||||
token_url: string(),
|
||||
user_info_url: string(),
|
||||
redirect_url: string(),
|
||||
scope: array(string())
|
||||
});
|
||||
}
|
||||
return object({});
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// User Model
|
||||
export const user = new ObjectModel({
|
||||
name: 'user',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
external_id: string(),
|
||||
provider: string(),
|
||||
displayName: string(),
|
||||
email: string()
|
||||
})
|
||||
});
|
||||
|
||||
// User Group Model
|
||||
export const userGroup = new ObjectModel({
|
||||
name: 'userGroup',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: string(),
|
||||
comment: defaulted(string(), ''),
|
||||
members: user.dynRef.refMany
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
//
|
||||
// Wireguard Models
|
||||
//
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
|
||||
// Wireguard Interface Model
|
||||
export const wireguardInterface = new ObjectModel({
|
||||
name: 'wireguardInterface',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: size(string(), 1, 128),
|
||||
comment: defaulted(size(string(), 0, 256), ''),
|
||||
enabled: defaulted(boolean(), true),
|
||||
privateKey: string(),
|
||||
publicKey: string(),
|
||||
ifName: string(),
|
||||
ifAddress: ipv4Cidr,
|
||||
listenPort: number(),
|
||||
endpoint: string(),
|
||||
dnsServer: nullable(ipv4Address)
|
||||
})
|
||||
});
|
||||
|
||||
// Wireguard Peer Model
|
||||
export const wireguardPeer = new ObjectModel({
|
||||
name: 'wireguardPeer',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: size(string(), 1, 128),
|
||||
comment: defaulted(size(string(), 0, 256), ''),
|
||||
interface: wireguardInterface.dynRef.refOne,
|
||||
enabled: defaulted(boolean(), true),
|
||||
|
||||
privateKey: string(),
|
||||
publicKey: string(),
|
||||
presharedKey: string(),
|
||||
ifAddress: ipv4Cidr,
|
||||
keepalive: size(defaulted(number(), 30), 1, 300),
|
||||
})
|
||||
});
|
||||
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
//
|
||||
// CMDB Models
|
||||
//
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
|
||||
// Address Object Model
|
||||
export const addressObject = new ObjectModel({
|
||||
name: 'addressObject',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: size(string(), 1, 128),
|
||||
comment: defaulted(size(string(), 0, 256), ''),
|
||||
elements: array(union([
|
||||
object({
|
||||
type: literal('ipmask'),
|
||||
ipmask: ipv4Address
|
||||
}),
|
||||
object({
|
||||
type: literal('fqdn'),
|
||||
fqdn: string(),
|
||||
result: nullable(defaulted(string(), null))
|
||||
}),
|
||||
object({
|
||||
type: literal('peer'),
|
||||
peer: wireguardPeer.dynRef.refOne
|
||||
})
|
||||
])),
|
||||
members: dynamic((memberList, context) => {
|
||||
// allow many values from list of Peer IDs
|
||||
// - exclude objects that contain members (loop prevention)
|
||||
// - exclude self (loop prevention)
|
||||
let permittedAdressObjects = Object.values(addressObject.getAll())
|
||||
.filter(object => object.members.length == 0)
|
||||
.filter(object => object.id != context.id)
|
||||
.map(item => item.id);
|
||||
|
||||
// return array from here to reduce Database reads
|
||||
return array(enums(permittedAdressObjects));
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
// Service Object Model
|
||||
export const serviceObject = new ObjectModel({
|
||||
name: 'serviceObject',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: size(string(), 1, 128),
|
||||
comment: defaulted(size(string(), 0, 256), ''),
|
||||
elements: array(union([
|
||||
object({
|
||||
type: literal('tcp'),
|
||||
port: number()
|
||||
}),
|
||||
object({
|
||||
type: literal('udp'),
|
||||
port: number()
|
||||
}),
|
||||
object({
|
||||
type: literal('icmp'),
|
||||
icmpType: enums([
|
||||
"echo-reply",
|
||||
"destination-unreachable",
|
||||
"source-quench",
|
||||
"redirect",
|
||||
"echo-request",
|
||||
"time-exceeded",
|
||||
"parameter-problem",
|
||||
"timestamp-request",
|
||||
"timestamp-reply",
|
||||
"info-request",
|
||||
"info-reply",
|
||||
"address-mask-request",
|
||||
"address-mask-reply",
|
||||
"router-advertisement",
|
||||
"router-solicitation"
|
||||
])
|
||||
}),
|
||||
object({
|
||||
type: literal('esp'),
|
||||
port: number()
|
||||
})
|
||||
]))
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
//
|
||||
// Firewall Policy Models
|
||||
//
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
|
||||
export const accessPolicy = new ObjectModel({
|
||||
name: 'accessPolicy',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: size(string(), 1, 128),
|
||||
comment: defaulted(size(string(), 0, 256), ''),
|
||||
enabled: defaulted(boolean(), true),
|
||||
|
||||
srcAddr: addressObject.dynRef.refMany,
|
||||
dstAddr: addressObject.dynRef.refMany,
|
||||
services: serviceObject.dynRef.refMany,
|
||||
})
|
||||
});
|
||||
|
||||
export const natPolicy = new ObjectModel({
|
||||
name: 'natPolicy',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
name: size(string(), 1, 128),
|
||||
comment: defaulted(size(string(), 0, 256), ''),
|
||||
enabled: defaulted(boolean(), true),
|
||||
|
||||
natType: enums(['snat', 'dnat', ' masquerade']),
|
||||
srcAddress: string(),
|
||||
dstAddress: string(),
|
||||
dnatPort: optional(number()), // Ziel Port für DNAT
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
//
|
||||
// Access Control Models
|
||||
//
|
||||
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||
|
||||
export const configToken = new ObjectModel({
|
||||
name: 'configToken',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
token: defaulted(string(), () => {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}),
|
||||
peer: string(),
|
||||
name: string()
|
||||
})
|
||||
});
|
||||
|
||||
export const apiToken = new ObjectModel({
|
||||
name: 'apiToken',
|
||||
model: object({
|
||||
id: defaulted(string(), () => {
|
||||
return crypto.randomUUID();
|
||||
}),
|
||||
token: defaulted(string(), () => {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}),
|
||||
name: string()
|
||||
}),
|
||||
hooks: {
|
||||
beforeCreate: (oldData, newData) => {
|
||||
console.log("before API Token created");
|
||||
},
|
||||
afterCreate: (oldData, newData) => {
|
||||
console.log("after API Token created");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default {
|
||||
wireguardInterface,
|
||||
wireguardPeer,
|
||||
addressObject,
|
||||
authProvider,
|
||||
user,
|
||||
userGroup,
|
||||
accessPolicy,
|
||||
natPolicy,
|
||||
configToken,
|
||||
apiToken
|
||||
}
|
||||
40
lib/nftables/config.mjs
Normal file
40
lib/nftables/config.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
wireguardInterface,
|
||||
wireguardPeer,
|
||||
addressObject,
|
||||
accessPolicy,
|
||||
natPolicy
|
||||
} from "../models.mjs";
|
||||
|
||||
import path from "node:path";
|
||||
import file from "node:fs";
|
||||
import ejs from "ejs";
|
||||
|
||||
|
||||
const configTemplatePath = path.join(process.cwd(), 'templates', 'nftables.ejs');
|
||||
|
||||
|
||||
export async function generateNftablesConfig(interfaceId) {
|
||||
const ifData = Object.values(wireguardInterface.getAll()).find(fi => fi.id == interfaceId);
|
||||
const addressObjects = addressObject.getAll();
|
||||
const accessPolicies = accessPolicy.getAll();
|
||||
const natPolicies = natPolicy.getAll();
|
||||
|
||||
const configData = {
|
||||
interface: ifData,
|
||||
peerList
|
||||
};
|
||||
|
||||
const configContent = await ejs.renderFile(
|
||||
configTemplatePath,
|
||||
configData,
|
||||
{
|
||||
async: true
|
||||
}
|
||||
);
|
||||
|
||||
file.writeFileSync(
|
||||
path.join(process.cwd(), 'data', 'nftables', `${ifData.ifName}.conf`),
|
||||
configContent
|
||||
)
|
||||
}
|
||||
34
lib/wireguard/config.mjs
Normal file
34
lib/wireguard/config.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
wireguardInterface,
|
||||
wireguardPeer
|
||||
} from "../models.mjs";
|
||||
|
||||
import path from "node:path";
|
||||
import file from "node:fs";
|
||||
import ejs from "ejs";
|
||||
|
||||
|
||||
const serverTemplatePath = path.join(process.cwd(), 'templates', 'wg_server.ejs');
|
||||
const clientTemplatePath = path.join(process.cwd(), 'templates', 'wg_client.ejs');
|
||||
|
||||
|
||||
export async function generateInterfaceConfig(interfaceId) {
|
||||
const ifData = Object.values(wireguardInterface.getAll()).find(fi => fi.id == interfaceId);
|
||||
const peerList = Object.values(wireguardPeer.getAll()).filter(fi => fi.interface == interfaceId);
|
||||
|
||||
let configData = await ejs.renderFile(
|
||||
serverTemplatePath,
|
||||
{
|
||||
interface: ifData,
|
||||
peerList
|
||||
},
|
||||
{
|
||||
async: true
|
||||
}
|
||||
);
|
||||
|
||||
file.writeFileSync(
|
||||
path.join(process.cwd(), 'data', 'wireguard', `${ifData.ifName}.conf`),
|
||||
configData
|
||||
)
|
||||
}
|
||||
45
lib/wireguard/process.mjs
Normal file
45
lib/wireguard/process.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
execSync as execCommand
|
||||
} from "node:child_process";
|
||||
|
||||
import {
|
||||
wireguardInterface
|
||||
} from "../models.mjs";
|
||||
|
||||
|
||||
|
||||
export async function startWireguardInterface(interfaceid) {
|
||||
try {
|
||||
const ifData = wireguardInterface.getById(interfaceid);
|
||||
const ifLink = getInterfaceLink(ifData.ifName);
|
||||
|
||||
if (ifLink.operstate == 'UP') {
|
||||
console.log(`Reloading interface ${ifData.ifName}`);
|
||||
console.log(`» reloading interface ${ifData.ifName}`);
|
||||
execCommand(`wg-quick up ${ifData.ifName}`, { stdio: 'inherit' });
|
||||
console.log(`✓ reloaded interface ${ifData.ifName}`);
|
||||
} else {
|
||||
console.log(`» starting interface ${ifData.ifName}`);
|
||||
execCommand(`wg-quick up ${ifData.ifName}`, { stdio: 'inherit' });
|
||||
console.log(`✓ started interface ${ifData.ifName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✕ failed to start interface ${iface}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getInterfaceLink(ifName) {
|
||||
try {
|
||||
let result = JSON.parse(
|
||||
execCommand(`ip -j link show`, { stdio: 'inherit' })
|
||||
).find(linkResult => linkResult.ifname == ifName);
|
||||
|
||||
if (!result) { return undefined; }
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`failed to get interface link for ${ifName}:`, error.message);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue