Minor update:

- Using a native solution instead of `Redis`
- Forcing logger to use custom log levels
- Fixing a few minor issues about generating configs
This commit is contained in:
Shahrad Elahi 2024-02-07 18:45:17 +03:30
parent 14624a6c12
commit 4875f6dea2
13 changed files with 161 additions and 176 deletions

View File

@ -15,11 +15,11 @@ RUN apk update && apk upgrade \
iproute2 iptables net-tools \ iproute2 iptables net-tools \
screen curl bash \ screen curl bash \
wireguard-tools \ wireguard-tools \
openssl \ tor &&\
tor \ # NPM packages
redis \ npm install -g @litehex/node-checksum &&\
# Clear APK cache # Clear APK cache
&& rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
COPY /config/torrc /etc/tor/torrc COPY /config/torrc /etc/tor/torrc

View File

@ -15,11 +15,11 @@ RUN apk update && apk upgrade \
iproute2 iptables net-tools \ iproute2 iptables net-tools \
screen vim curl bash \ screen vim curl bash \
wireguard-tools \ wireguard-tools \
openssl \ tor &&\
tor \ # NPM packages
redis \ npm install -g @litehex/node-checksum &&\
# Clear APK cache # Clear APK cache
&& rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
COPY /config/torrc /etc/tor/torrc COPY /config/torrc /etc/tor/torrc
COPY /config/obfs4-bridges.conf /etc/torrc.d/obfs4-bridges.conf COPY /config/obfs4-bridges.conf /etc/torrc.d/obfs4-bridges.conf
@ -34,6 +34,9 @@ ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable
ENV NODE_ENV=development
ENV LOG_LEVEL=debug
COPY docker-entrypoint.sh /entrypoint.sh COPY docker-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]

View File

@ -45,13 +45,13 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"dependencies": { "dependencies": {
"@litehex/storage-box": "^0.1.1-canary.0",
"bits-ui": "^0.17.0", "bits-ui": "^0.17.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"execa": "^8.0.1", "execa": "^8.0.1",
"formsnap": "^0.4.3", "formsnap": "^0.4.3",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.323.0", "lucide-svelte": "^0.323.0",
"node-netkit": "0.1.0-canary.1", "node-netkit": "0.1.0-canary.1",

View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@litehex/storage-box':
specifier: ^0.1.1-canary.0
version: 0.1.1-canary.0
bits-ui: bits-ui:
specifier: ^0.17.0 specifier: ^0.17.0
version: 0.17.0(svelte@4.2.10) version: 0.17.0(svelte@4.2.10)
@ -23,9 +26,6 @@ dependencies:
formsnap: formsnap:
specifier: ^0.4.3 specifier: ^0.4.3
version: 0.4.3(svelte@4.2.10)(sveltekit-superforms@1.13.4)(zod@3.22.4) version: 0.4.3(svelte@4.2.10)(sveltekit-superforms@1.13.4)(zod@3.22.4)
ioredis:
specifier: ^5.3.2
version: 5.3.2
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
@ -361,10 +361,6 @@ packages:
'@swc/helpers': 0.5.3 '@swc/helpers': 0.5.3
dev: false dev: false
/@ioredis/commands@1.2.0:
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
dev: false
/@jridgewell/gen-mapping@0.3.3: /@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@ -390,6 +386,14 @@ packages:
'@jridgewell/resolve-uri': 3.1.1 '@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
/@litehex/storage-box@0.1.1-canary.0:
resolution: { integrity: sha512-wUSYhr8jdbrBDeKu0HHmaXVLKAJWXtyD41g/6deCROQ0OwyQwNPCOWwAjMVzdrIpTo6fyGl+QliMxyNSUqbCqw== }
dependencies:
'@msgpack/msgpack': 3.0.0-beta2
debounce: 2.0.0
type-fest: 4.10.2
dev: false
/@melt-ui/svelte@0.71.2(svelte@4.2.10): /@melt-ui/svelte@0.71.2(svelte@4.2.10):
resolution: { integrity: sha512-GDUErhAphEoEOLpcBjQ84BgzRR6M3344fQE4QYFffwT7aedWak7CvNsECgeig1Y5xvfDmeEaFnGlOQXIBucJYw== } resolution: { integrity: sha512-GDUErhAphEoEOLpcBjQ84BgzRR6M3344fQE4QYFffwT7aedWak7CvNsECgeig1Y5xvfDmeEaFnGlOQXIBucJYw== }
peerDependencies: peerDependencies:
@ -404,6 +408,11 @@ packages:
svelte: 4.2.10 svelte: 4.2.10
dev: false dev: false
/@msgpack/msgpack@3.0.0-beta2:
resolution: { integrity: sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw== }
engines: { node: '>= 14' }
dev: false
/@nodelib/fs.scandir@2.1.5: /@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1042,11 +1051,6 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: false dev: false
/cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
dev: false
/code-red@1.0.4: /code-red@1.0.4:
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
dependencies: dependencies:
@ -1109,6 +1113,11 @@ packages:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
dev: false dev: false
/debounce@2.0.0:
resolution: { integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA== }
engines: { node: '>=18' }
dev: false
/debug@4.3.4(supports-color@8.1.1): /debug@4.3.4(supports-color@8.1.1):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -1140,11 +1149,6 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
/denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
dev: false
/dequal@2.0.3: /dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1503,23 +1507,6 @@ packages:
/inherits@2.0.4: /inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
/ioredis@5.3.2:
resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==}
engines: {node: '>=12.22.0'}
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.3.4(supports-color@8.1.1)
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
dev: false
/is-binary-path@2.1.0: /is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1673,18 +1660,10 @@ packages:
p-locate: 5.0.0 p-locate: 5.0.0
dev: true dev: true
/lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
dev: false
/lodash.includes@4.3.0: /lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false dev: false
/lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
dev: false
/lodash.isboolean@3.0.3: /lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
dev: false dev: false
@ -2227,18 +2206,6 @@ packages:
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
dev: false dev: false
/redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
dev: false
/redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
dependencies:
redis-errors: 1.2.0
dev: false
/regenerator-runtime@0.14.0: /regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false dev: false
@ -2425,10 +2392,6 @@ packages:
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
dev: false dev: false
/standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
dev: false
/string-width@4.2.3: /string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2732,6 +2695,11 @@ packages:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/type-fest@4.10.2:
resolution: { integrity: sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw== }
engines: { node: '>=16' }
dev: false
/typescript@5.3.3: /typescript@5.3.3:
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}

View File

@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { sha256 } from '$lib/hash'; import { sha256 } from '$lib/hash';
import { getClient } from '$lib/redis'; import { client } from '$lib/storage';
import 'dotenv/config'; import 'dotenv/config';
export const AUTH_SECRET = process.env.AUTH_SECRET || sha256(randomUUID()); export const AUTH_SECRET = process.env.AUTH_SECRET || sha256(randomUUID());
@ -17,8 +17,7 @@ export async function generateToken(): Promise<string> {
}, },
AUTH_SECRET, AUTH_SECRET,
); );
const client = getClient(); client.setex(token, '1', oneHour);
await client.setex(token, oneHour, '1');
return token; return token;
} }
@ -26,15 +25,14 @@ export async function verifyToken(token: string): Promise<boolean> {
try { try {
const decode = jwt.verify(token, AUTH_SECRET); const decode = jwt.verify(token, AUTH_SECRET);
if (!decode) return false; if (!decode) return false;
const client = getClient();
const exists = await client.exists(token); const exists = client.exists(token);
return exists === 1; return exists;
} catch (e) { } catch (e) {
return false; return false;
} }
} }
export async function revokeToken(token: string): Promise<void> { export async function revokeToken(token: string): Promise<void> {
const client = getClient(); client.del(token);
await client.del(token);
} }

View File

@ -1,4 +1,4 @@
import pino, { type Logger } from 'pino'; import pino, { type Logger, type LoggerOptions } from 'pino';
import pretty from 'pino-pretty'; import pretty from 'pino-pretty';
import { createWriteStream } from 'node:fs'; import { createWriteStream } from 'node:fs';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
@ -8,23 +8,33 @@ const LOG_LEVEL = process.env.LOG_LEVEL || 'trace';
const LOG_FILE_PATH = process.env.LOG_FILE_PATH || '/var/vlogs/web'; const LOG_FILE_PATH = process.env.LOG_FILE_PATH || '/var/vlogs/web';
const LOG_COLORS = process.env.LOG_COLORS || 'true'; const LOG_COLORS = process.env.LOG_COLORS || 'true';
const options: LoggerOptions = {
level: LOG_LEVEL,
customLevels: {
http: 10,
info: 30,
debug: 35,
warn: 40,
error: 50,
fatal: 60,
},
useOnlyCustomLevels: true,
};
const jsonLevels = JSON.stringify(options.customLevels);
const levelsInString = jsonLevels.replaceAll('"', '').slice(0, -1).slice(1);
const prettyStream = pretty({ const prettyStream = pretty({
colorize: LOG_COLORS === 'true', colorize: LOG_COLORS === 'true',
customLevels: levelsInString,
}); });
let logger: Logger = pino( let logger: Logger = pino(options, pino.multistream([prettyStream]));
{
level: LOG_LEVEL,
},
pino.multistream([prettyStream]),
);
if (fsAccess(LOG_FILE_PATH)) { if (fsAccess(LOG_FILE_PATH)) {
fsTouch(LOG_FILE_PATH).then(() => { fsTouch(LOG_FILE_PATH).then(() => {
logger = pino( logger = pino(
{ options,
level: LOG_LEVEL,
},
pino.multistream([ pino.multistream([
prettyStream, prettyStream,
createWriteStream(resolve(LOG_FILE_PATH), { createWriteStream(resolve(LOG_FILE_PATH), {

View File

@ -1,25 +0,0 @@
import { Redis } from 'ioredis';
export type RedisClient = Redis;
let client: RedisClient | undefined;
export function getClient(): RedisClient {
if (!client) {
throw new Error('Redis client not initialized');
}
return client;
}
export function setClient(redis: RedisClient): void {
client = redis;
}
if (process.env.NODE_ENV && ['development', 'production'].includes(process.env.NODE_ENV)) {
setClient(
new Redis({
port: 6479,
}),
);
}

8
web/src/lib/storage.ts Normal file
View File

@ -0,0 +1,8 @@
import { Client, MSGPack } from '@litehex/storage-box';
import { FsDriver } from '@litehex/storage-box/driver';
import { resolve } from 'node:path';
const storagePath = resolve(process.cwd(), 'storage.pack');
const driver = new FsDriver(storagePath, { parser: MSGPack });
export const client = new Client(driver);

View File

@ -31,6 +31,10 @@ export function isObject(obj: object) {
return Object.prototype.toString.call(obj) === '[object Object]'; return Object.prototype.toString.call(obj) === '[object Object]';
} }
export async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** /**
* Private IP Address Identifier in Regular Expression * Private IP Address Identifier in Regular Expression
* *

View File

@ -4,12 +4,12 @@ import deepmerge from 'deepmerge';
import type { Peer, WgKey, WgServer } from '$lib/typings'; import type { Peer, WgKey, WgServer } from '$lib/typings';
import Network from '$lib/network'; import Network from '$lib/network';
import { WG_PATH, WG_SEVER_PATH } from '$lib/constants'; import { WG_PATH, WG_SEVER_PATH } from '$lib/constants';
import { dynaJoin, isJson } from '$lib/utils'; import { dynaJoin, isJson, sleep } from '$lib/utils';
import { getPeerConf } from '$lib/wireguard/utils'; import { getPeerConf } from '$lib/wireguard/utils';
import logger from '$lib/logger'; import logger from '$lib/logger';
import { sha256 } from '$lib/hash'; import { sha256 } from '$lib/hash';
import { fsAccess } from '$lib/fs-extra'; import { fsAccess } from '$lib/fs-extra';
import { getClient } from '$lib/redis'; import { client } from '$lib/storage';
import { execa } from 'execa'; import { execa } from 'execa';
export class WGServer { export class WGServer {
@ -17,30 +17,42 @@ export class WGServer {
readonly peers: WGPeers; readonly peers: WGPeers;
constructor(serverId: string) { constructor(serverId: string) {
if (!serverId) throw new Error('serverId is required'); if (!serverId) {
throw new Error('WGServer: id is required');
}
if (!WGServer.exists(serverId)) throw new Error('server does not exists'); if (!WGServer.exists(serverId)) {
throw new Error('WGServer: server not found');
}
this.id = serverId; this.id = serverId;
this.peers = new WGPeers(this); this.peers = new WGPeers(this);
} }
static async exists(id: string): Promise<boolean> { static exists(id: string): boolean {
const servers = await getServers(); const serverIds = getServers().map((s) => s.id);
return servers.some((s) => s.id === id);
logger.debug({
message: `WGServer:Exists: checking for server with id: ${id}`,
servers: serverIds,
});
return serverIds.includes(id);
} }
async get(): Promise<WgServer> { async get(): Promise<WgServer> {
if (!fsAccess(WG_PATH)) { if (!fsAccess(WG_PATH)) {
logger.debug('WGServer: get: creating wg path');
fs.mkdirSync(WG_PATH, { recursive: true, mode: 0o600 }); fs.mkdirSync(WG_PATH, { recursive: true, mode: 0o600 });
} }
const server = await findServer(this.id); const server = await findServer(this.id);
if (!server) { if (!server) {
throw new Error('server not found'); throw new Error('WGServer: get: server not found');
} }
if (!fsAccess(resolveConfigPath(server.confId))) { if (!fsAccess(resolveConfigPath(server.confId))) {
logger.debug('WGServer: get: creating config file');
await this.writeConfigFile(server); await this.writeConfigFile(server);
} }
@ -83,24 +95,19 @@ export class WGServer {
const server = await this.get(); const server = await this.get();
await this.stop(); await this.stop();
if (wgConfExists(server.confId)) { if (wgConfExists(server.confId)) {
logger.debug('WGServer:Remove: removing config file');
fs.unlinkSync(resolveConfigPath(server.confId)); fs.unlinkSync(resolveConfigPath(server.confId));
} }
const index = await findServerIndex(this.id); const index = await findServerIndex(this.id);
if (typeof index !== 'number') { if (typeof index !== 'number') {
logger.warn('findServerIndex: index not found'); logger.warn('WGServer:Remove: server index not found');
return true; return true;
} }
const client = getClient(); client.ldel(WG_SEVER_PATH, index);
const element = await client.lindex(WG_SEVER_PATH, index);
if (!element) {
logger.warn('remove: element not found');
return true;
}
await client.lrem(WG_SEVER_PATH, 1, element);
return true; return true;
} }
@ -110,12 +117,14 @@ export class WGServer {
const index = await findServerIndex(this.id); const index = await findServerIndex(this.id);
if (typeof index !== 'number') { if (typeof index !== 'number') {
logger.warn('findServerIndex: index not found'); logger.warn('WGServer:Update: server index not found');
return true; return true;
} }
const client = getClient(); console.log('WGServer:Update: updating server at index:', index);
const res = await client.lset( console.log(client.lrange(WG_SEVER_PATH, 0, -1));
client.lset(
WG_SEVER_PATH, WG_SEVER_PATH,
index, index,
JSON.stringify({ JSON.stringify({
@ -125,7 +134,7 @@ export class WGServer {
}), }),
); );
return res === 'OK'; return true;
} }
async writeConfigFile(wg: WgServer): Promise<void> { async writeConfigFile(wg: WgServer): Promise<void> {
@ -154,7 +163,7 @@ export class WGServer {
}; };
if (!hasInterface) { if (!hasInterface) {
logger.debug('GetUsage: interface does not exists'); logger.debug('WGServer: GetUsage: interface does not exists');
return usages; return usages;
} }
@ -181,7 +190,7 @@ export class WGServer {
static async getFreePeerIp(serverId: string): Promise<string | undefined> { static async getFreePeerIp(serverId: string): Promise<string | undefined> {
const server = await findServer(serverId); const server = await findServer(serverId);
if (!server) { if (!server) {
logger.error('GetFreePeerIP: no server found'); logger.error('WGSerevr: GetFreePeerIP: no server found');
return undefined; return undefined;
} }
@ -196,7 +205,7 @@ export class WGServer {
} }
} }
logger.error('GetFreePeerIP: no free ip found'); logger.error('WGServer: GetFreePeerIP: no free ip found');
return undefined; return undefined;
} }
} }
@ -239,12 +248,11 @@ class WGPeers {
const index = await findServerIndex(this.server.id); const index = await findServerIndex(this.server.id);
if (typeof index !== 'number') { if (typeof index !== 'number') {
logger.warn('findServerIndex: index not found'); logger.warn('WGPeers:Add: server index not found');
return true; return true;
} }
const client = getClient(); client.lset(
await client.lset(
WG_SEVER_PATH, WG_SEVER_PATH,
index, index,
JSON.stringify({ JSON.stringify({
@ -263,16 +271,15 @@ class WGPeers {
async remove(publicKey: string): Promise<boolean> { async remove(publicKey: string): Promise<boolean> {
const server = await this.server.get(); const server = await this.server.get();
const peers = await wgPeersStr(server.confId); const peers = wgPeersStr(server.confId);
const index = await findServerIndex(this.server.id); const index = await findServerIndex(this.server.id);
if (typeof index !== 'number') { if (typeof index !== 'number') {
logger.warn('findServerIndex: index not found'); logger.warn('WGPeers:Remove: server index not found');
return true; return true;
} }
const client = getClient(); client.lset(
await client.lset(
WG_SEVER_PATH, WG_SEVER_PATH,
index, index,
JSON.stringify({ JSON.stringify({
@ -283,7 +290,7 @@ class WGPeers {
const peerIndex = peers.findIndex((p) => p.includes(`PublicKey = ${publicKey}`)); const peerIndex = peers.findIndex((p) => p.includes(`PublicKey = ${publicKey}`));
if (peerIndex === -1) { if (peerIndex === -1) {
logger.warn('removePeer: no peer found'); logger.warn('WGPeers:Remove: peer not found');
return false; return false;
} }
@ -307,7 +314,7 @@ class WGPeers {
const index = await findServerIndex(this.server.id); const index = await findServerIndex(this.server.id);
if (typeof index !== 'number') { if (typeof index !== 'number') {
logger.warn('findServerIndex: index not found'); logger.warn('WGPeers:Update: server index not found');
return true; return true;
} }
@ -316,8 +323,7 @@ class WGPeers {
return deepmerge(p, update); return deepmerge(p, update);
}); });
const client = getClient(); client.lset(WG_SEVER_PATH, index, JSON.stringify({ ...server, peers: updatedPeers }));
await client.lset(WG_SEVER_PATH, index, JSON.stringify({ ...server, peers: updatedPeers }));
await this.storePeers(publicKey, updatedPeers); await this.storePeers(publicKey, updatedPeers);
if (server.status === 'up') { if (server.status === 'up') {
@ -336,14 +342,16 @@ class WGPeers {
async generateConfig(peerId: string): Promise<string | undefined> { async generateConfig(peerId: string): Promise<string | undefined> {
const server = await findServer(this.server.id); const server = await findServer(this.server.id);
if (!server) { if (!server) {
logger.error('generatePeerConfig: server not found'); logger.error('WGPeers:GeneratePeerConfig: server not found');
return undefined; return undefined;
} }
const peer = server.peers.find((p) => p.id === peerId); const peer = server.peers.find((p) => p.id === peerId);
if (!peer) { if (!peer) {
logger.error('generatePeerConfig: peer not found'); logger.error('WGPeers:GeneratePeerConfig: peer not found');
return undefined; return undefined;
} }
return await getPeerConf({ return await getPeerConf({
...peer, ...peer,
serverPublicKey: server.publicKey, serverPublicKey: server.publicKey,
@ -487,11 +495,14 @@ async function syncServers(): Promise<boolean> {
// read all confs // read all confs
const servers = await Promise.all(confs.map((f) => readWgConf(parseInt(f.match(reg)![1])))); const servers = await Promise.all(confs.map((f) => readWgConf(parseInt(f.match(reg)![1]))));
const client = getClient();
// remove old servers // remove old servers
await client.del(WG_SEVER_PATH); client.del(WG_SEVER_PATH);
// save all servers to redis // save all servers to redis
await client.lpush(WG_SEVER_PATH, ...servers.map((s) => JSON.stringify(s))); client.lpush(
WG_SEVER_PATH,
servers.map((s) => JSON.stringify(s)),
);
return true; return true;
} }
@ -519,16 +530,19 @@ interface GenerateWgServerParams {
port: number; port: number;
dns?: string; dns?: string;
mtu?: number; mtu?: number;
insertDb?: boolean;
} }
export async function generateWgServer(config: GenerateWgServerParams): Promise<string> { export async function generateWgServer(config: GenerateWgServerParams): Promise<string> {
const { privateKey, publicKey } = await generateWgKey(); const { privateKey, publicKey } = await generateWgKey();
// inside redis create a config list // Inside storage create a config list
const confId = await getNextFreeConfId(); const confId = await getNextFreeConfId();
const uuid = crypto.randomUUID(); const uuid = crypto.randomUUID();
logger.debug(
`WireGuard: GenerateWgServer: creating server with id: ${uuid} and confId: ${confId}`,
);
let server: WgServer = { let server: WgServer = {
id: uuid, id: uuid,
confId, confId,
@ -550,7 +564,7 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise<
status: 'up', status: 'up',
}; };
// check if address or port are already reserved // Check if address or port is already reserved
if (await isIPReserved(config.address)) { if (await isIPReserved(config.address)) {
throw new Error(`Address ${config.address} is already reserved!`); throw new Error(`Address ${config.address} is already reserved!`);
} }
@ -565,17 +579,20 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise<
server.postDown = iptables.down; server.postDown = iptables.down;
// save server config // save server config
if (false !== config.insertDb) { logger.debug('WireGuard: GenerateWgServer: saving server to storage');
const client = getClient(); logger.debug(server);
await client.lpush(WG_SEVER_PATH, JSON.stringify(server)); client.lpush(WG_SEVER_PATH, JSON.stringify(server));
}
const CONFIG_PATH = resolveConfigPath(confId); const CONFIG_PATH = resolveConfigPath(confId);
// save server config to disk // save server config to disk
logger.debug('WireGuard: GenerateWgServer: writing config file');
fs.writeFileSync(CONFIG_PATH, await genServerConf(server), { mode: 0o600 }); fs.writeFileSync(CONFIG_PATH, await genServerConf(server), { mode: 0o600 });
await sleep(50);
// updating hash of the config // updating hash of the config
logger.debug('WireGuard: GenerateWgServer: updating config hash');
const wg = new WGServer(uuid); const wg = new WGServer(uuid);
await wg.update({ confHash: getConfigHash(confId) }); await wg.update({ confHash: getConfigHash(confId) });
@ -590,20 +607,17 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise<
} }
export async function isIPReserved(ip: string): Promise<boolean> { export async function isIPReserved(ip: string): Promise<boolean> {
const addresses = (await getServers()).map((s) => s.address); const addresses = getServers().map((s) => s.address);
return addresses.includes(ip); return addresses.includes(ip);
} }
export async function isPortReserved(port: number): Promise<boolean> { export async function isPortReserved(port: number): Promise<boolean> {
const inUsePorts = [ const inUsePorts = [await Network.inUsePorts(), getServers().map((s) => Number(s.listen))].flat();
await Network.inUsePorts(),
(await getServers()).map((s) => Number(s.listen)),
].flat();
return inUsePorts.includes(port); return inUsePorts.includes(port);
} }
export async function isConfigIdReserved(id: number): Promise<boolean> { export async function isConfigIdReserved(id: number): Promise<boolean> {
const ids = (await getServers()).map((s) => s.confId); const ids = getServers().map((s) => s.confId);
return ids.includes(id); return ids.includes(id);
} }
@ -619,6 +633,7 @@ export async function getNextFreeConfId(): Promise<number> {
export function getConfigHash(confId: number): string | undefined { export function getConfigHash(confId: number): string | undefined {
if (!wgConfExists(confId)) { if (!wgConfExists(confId)) {
logger.debug('WireGuard: GetConfigHash: config does not exists. ConfId:', confId);
return undefined; return undefined;
} }
@ -645,10 +660,21 @@ export function maxConfId(): number {
return Math.max(0, ...ids); return Math.max(0, ...ids);
} }
export async function getServers(): Promise<WgServer[]> { export function getServers(): WgServer[] {
const client = getClient(); const rawServers = (client.list(WG_SEVER_PATH) || []) as string[];
const rawServers = await client.lrange(WG_SEVER_PATH, 0, -1); return rawServers.map((s) => {
return rawServers.map((s) => JSON.parse(s)); if (isJson(s)) {
return JSON.parse(s);
}
if (typeof s === 'object') {
return s;
}
logger.warn('WireGuard: GetServers: invalid server found');
return null;
});
} }
export async function findServerIndex(id: string): Promise<number | undefined> { export async function findServerIndex(id: string): Promise<number | undefined> {

View File

@ -15,7 +15,7 @@ import logger from '$lib/logger';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
return { return {
servers: (await getServers()).map((s) => s), servers: getServers(),
form: superValidate(CreateServerSchema), form: superValidate(CreateServerSchema),
}; };
}; };

View File

@ -70,12 +70,6 @@
<i class={'fa-solid fa-onion text-purple-700 text-xl'} /> <i class={'fa-solid fa-onion text-purple-700 text-xl'} />
</svelte:fragment> </svelte:fragment>
</Service> </Service>
<Service name="Redis" slug="redis">
<svelte:fragment slot="icon">
<i class={'fa-solid fa-database text-red-700 text-xl'} />
</svelte:fragment>
</Service>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import PageFooter from '$lib/components/page/PageFooter.svelte'; import PageFooter from '$lib/components/page/PageFooter.svelte';
import Logo from '../../../static/logo.png';
</script> </script>
<div class={'w-full min-h-screen flex justify-center px-2 md:px-6 py-2'}> <div class={'w-full min-h-screen flex justify-center px-2 md:px-6 py-2'}>
<div class={'w-full mx-auto max-w-3xl flex flex-col items-center gap-y-3.5'}> <div class={'w-full mx-auto max-w-3xl flex flex-col items-center gap-y-3.5'}>
<header class={'flex items-center gap-x-2 text-3xl font-medium py-4'}> <header class={'flex items-center gap-x-2 text-3xl font-medium py-4'}>
<img src={Logo} alt="WireAdmin" width="40" height="40" /> <img src={'/logo.png'} alt="WireAdmin" width="40" height="40" />
<h1>WireAdmin</h1> <h1>WireAdmin</h1>
</header> </header>
<main class={'py-4'}> <main class={'py-4'}>