From 42bc3dc971854db4247d80b2725b3fd64b10039e Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Tue, 19 Dec 2023 13:15:03 +0330 Subject: [PATCH] better code quality and fixes minor-known issues --- web/src/lib/fs-extra.ts | 6 +- web/src/lib/logger.ts | 7 +- web/src/lib/shell.ts | 2 +- web/src/lib/wireguard/index.ts | 296 +++++++++++----------- web/src/routes/+page.server.ts | 3 +- web/src/routes/[serverId]/+page.server.ts | 24 +- web/src/routes/api/health/+server.ts | 8 +- web/src/routes/login/+page.server.ts | 8 +- 8 files changed, 184 insertions(+), 170 deletions(-) diff --git a/web/src/lib/fs-extra.ts b/web/src/lib/fs-extra.ts index 80125b1..60c50f9 100644 --- a/web/src/lib/fs-extra.ts +++ b/web/src/lib/fs-extra.ts @@ -1,8 +1,8 @@ -import { promises } from 'fs'; +import { accessSync, promises } from 'fs'; -export async function fsAccess(path: string): Promise { +export function fsAccess(path: string): boolean { try { - await promises.access(path); + accessSync(path); return true; } catch (error) { return false; diff --git a/web/src/lib/logger.ts b/web/src/lib/logger.ts index 78e435f..3d85210 100644 --- a/web/src/lib/logger.ts +++ b/web/src/lib/logger.ts @@ -5,7 +5,7 @@ import { resolve } from 'node:path'; import { fsAccess, fsTouch } from '$lib/fs-extra'; const LOG_LEVEL = process.env.LOG_LEVEL || 'trace'; -const LOG_FILE_PATH = process.env.LOG_FILE_PATH || '/var/vlogs/web.log'; +const LOG_FILE_PATH = process.env.LOG_FILE_PATH || '/var/vlogs/web'; const LOG_COLORS = process.env.LOG_COLORS || 'true'; const prettyStream = pretty({ @@ -24,6 +24,7 @@ fsTouch(LOG_FILE_PATH) .then((ok) => { if (!ok) { logger.warn('Log file is not accessible'); + return; } logger = pino( { @@ -37,8 +38,6 @@ fsTouch(LOG_FILE_PATH) ]), ); }) - .catch((error) => { - logger.error(error); - }); + .catch(console.error); export default logger; diff --git a/web/src/lib/shell.ts b/web/src/lib/shell.ts index 9fed824..2f4a4e5 100644 --- a/web/src/lib/shell.ts +++ b/web/src/lib/shell.ts @@ -23,7 +23,7 @@ export default class Shell { )}`; if (safe) { - logger.warn(message); + logger.debug(message); return resolve(stderr); } diff --git a/web/src/lib/wireguard/index.ts b/web/src/lib/wireguard/index.ts index bb92566..b3bfbe9 100644 --- a/web/src/lib/wireguard/index.ts +++ b/web/src/lib/wireguard/index.ts @@ -1,8 +1,6 @@ import fs from 'fs'; import path from 'path'; import deepmerge from 'deepmerge'; -import SHA256 from 'crypto-js/sha256'; -import Hex from 'crypto-js/enc-hex'; import type { Peer, WgKey, WgServer } from '$lib/typings'; import Network from '$lib/network'; import Shell from '$lib/shell'; @@ -10,34 +8,62 @@ import { WG_PATH } from '$lib/constants'; import { client, WG_SEVER_PATH } from '$lib/redis'; import { dynaJoin, isJson } from '$lib/utils'; import { getPeerConf } from '$lib/wireguard/utils'; +import logger from '$lib/logger'; +import { sha256 } from '$lib/hash'; +import { fsAccess } from '$lib/fs-extra'; export class WGServer { - static async stop(id: string): Promise { - const server = await findServer(id); - if (!server) { - console.error('server could not be updated (reason: not exists)'); - return false; + readonly id: string; + readonly peers: WGPeers; + + constructor(serverId: string) { + if (!serverId) throw new Error('serverId is required'); + + if (!WGServer.exists(serverId)) throw new Error('server does not exists'); + + this.id = serverId; + this.peers = new WGPeers(this); + } + + static async exists(id: string): Promise { + const servers = await getServers(); + return servers.some((s) => s.id === id); + } + + async get(): Promise { + if (!fsAccess(WG_PATH)) { + fs.mkdirSync(WG_PATH, { recursive: true, mode: 0o600 }); } + const server = await findServer(this.id); + if (!server) { + throw new Error('server not found'); + } + + if (!fsAccess(resolveConfigPath(server.confId))) { + await this.writeConfigFile(server); + } + + return server; + } + + async stop(): Promise { + const server = await this.get(); + if (await Network.checkInterfaceExists(`wg${server.confId}`)) { await Shell.exec(`wg-quick down wg${server.confId}`, true); } - await this.update(id, { status: 'down' }); + await this.update({ status: 'down' }); return true; } - static async start(id: string): Promise { - const server = await findServer(id); - if (!server) { - console.error('server could not be updated (reason: not exists)'); - return false; - } + async start(): Promise { + const server = await this.get(); const HASH = getConfigHash(server.confId); if (!HASH || server.confHash !== HASH) { - await writeConfigFile(server); - await WGServer.update(id, { confHash: getConfigHash(server.confId) }); + await this.writeConfigFile(server); } if (await Network.checkInterfaceExists(`wg${server.confId}`)) { @@ -46,31 +72,27 @@ export class WGServer { await Shell.exec(`wg-quick up wg${server.confId}`); - await this.update(id, { status: 'up' }); + await this.update({ status: 'up' }); return true; } - static async remove(id: string): Promise { - const server = await findServer(id); - if (!server) { - console.error('server could not be updated (reason: not exists)'); - return false; - } + async remove(): Promise { + const server = await this.get(); - await this.stop(id); + await this.stop(); if (wgConfExists(server.confId)) { - fs.unlinkSync(path.join(WG_PATH, `wg${server.confId}.conf`)); + fs.unlinkSync(resolveConfigPath(server.confId)); } - const index = await findServerIndex(id); + const index = await findServerIndex(this.id); if (typeof index !== 'number') { - console.warn('findServerIndex: index not found'); + logger.warn('findServerIndex: index not found'); return true; } const element = await client.lindex(WG_SEVER_PATH, index); if (!element) { - console.warn('remove: element not found'); + logger.warn('remove: element not found'); return true; } @@ -79,17 +101,15 @@ export class WGServer { return true; } - static async update(id: string, update: Partial): Promise { - const server = await findServer(id); - if (!server) { - console.error('server could not be updated (reason: not exists)'); - return false; - } - const index = await findServerIndex(id); + async update(update: Partial): Promise { + const server = await this.get(); + + const index = await findServerIndex(this.id); if (typeof index !== 'number') { - console.warn('findServerIndex: index not found'); + logger.warn('findServerIndex: index not found'); return true; } + const res = await client.lset( WG_SEVER_PATH, index, @@ -99,22 +119,50 @@ export class WGServer { updatedAt: new Date().toISOString(), }), ); + return res === 'OK'; } - static async findAttachedUuid(confId: number): Promise { - const server = await getServers(); - return server.find((s) => s.confId === confId)?.id; + async writeConfigFile(wg: WgServer): Promise { + const CONFIG_PATH = resolveConfigPath(wg.confId); + fs.writeFileSync(CONFIG_PATH, await genServerConf(wg), { mode: 0o600 }); + await this.update({ confHash: getConfigHash(wg.confId) }); } - static async addPeer(id: string, peer: WgServer['peers'][0]): Promise { - const server = await findServer(id); + static async getFreePeerIp(serverId: string): Promise { + const server = await findServer(serverId); if (!server) { - console.error('server could not be updated (reason: not exists)'); - return false; + logger.error('GetFreePeerIP: no server found'); + return undefined; } - const confPath = path.join(WG_PATH, `wg${server.confId}.conf`); + const reservedIps = server.peers.map((p) => p.allowedIps); + const ips = reservedIps.map((ip) => ip.split('/')[0]); + const net = server.address.split('/')[0].split('.'); + + for (let i = 1; i < 255; i++) { + const ip = `${net[0]}.${net[1]}.${net[2]}.${i}`; + if (!ips.includes(ip) && ip !== server.address.split('/')[0]) { + return ip; + } + } + + logger.error('GetFreePeerIP: no free ip found'); + return undefined; + } +} + +class WGPeers { + private readonly server: WGServer; + + constructor(server: WGServer) { + this.server = server; + } + + async add(peer: WgServer['peers'][0]): Promise { + const server = await this.server.get(); + + const confPath = resolveConfigPath(server.confId); const conf = fs.readFileSync(confPath, 'utf-8'); const lines = conf.split('\n'); @@ -128,13 +176,14 @@ export class WGServer { ]), ); fs.writeFileSync(confPath, lines.join('\n'), { mode: 0o600 }); - await WGServer.update(id, { confHash: getConfigHash(server.confId) }); + await this.server.update({ confHash: getConfigHash(server.confId) }); - const index = await findServerIndex(id); + const index = await findServerIndex(this.server.id); if (typeof index !== 'number') { - console.warn('findServerIndex: index not found'); + logger.warn('findServerIndex: index not found'); return true; } + await client.lset( WG_SEVER_PATH, index, @@ -145,24 +194,20 @@ export class WGServer { ); if (server.status === 'up') { - await this.stop(server.id); - await this.start(server.id); + await this.server.stop(); + await this.server.start(); } return true; } - static async removePeer(serverId: string, publicKey: string): Promise { - const server = await findServer(serverId); - if (!server) { - console.error('server could not be updated (reason: not exists)'); - return false; - } + async remove(publicKey: string): Promise { + const server = await this.server.get(); const peers = await wgPeersStr(server.confId); - const index = await findServerIndex(serverId); + const index = await findServerIndex(this.server.id); if (typeof index !== 'number') { - console.warn('findServerIndex: index not found'); + logger.warn('findServerIndex: index not found'); return true; } await client.lset( @@ -176,35 +221,31 @@ export class WGServer { const peerIndex = peers.findIndex((p) => p.includes(`PublicKey = ${publicKey}`)); if (peerIndex === -1) { - console.warn('removePeer: no peer found'); + logger.warn('removePeer: no peer found'); return false; } - const confPath = path.join(WG_PATH, `wg${server.confId}.conf`); + const confPath = resolveConfigPath(server.confId); const conf = fs.readFileSync(confPath, 'utf-8'); const serverConfStr = conf.includes('[Peer]') ? conf.split('[Peer]')[0] : conf; const peersStr = peers.filter((_, i) => i !== peerIndex).join('\n'); fs.writeFileSync(confPath, `${serverConfStr}\n${peersStr}`, { mode: 0o600 }); - await WGServer.update(server.id, { confHash: getConfigHash(server.confId) }); + await this.server.update({ confHash: getConfigHash(server.confId) }); if (server.status === 'up') { - await this.stop(server.id); - await this.start(server.id); + await this.server.stop(); + await this.server.start(); } return true; } - static async updatePeer(serverId: string, publicKey: string, update: Partial): Promise { - const server = await findServer(serverId); - if (!server) { - console.error('WGServer:UpdatePeer: server could not be updated (Reason: not exists)'); - return false; - } + async update(publicKey: string, update: Partial): Promise { + const server = await this.server.get(); - const index = await findServerIndex(serverId); + const index = await findServerIndex(this.server.id); if (typeof index !== 'number') { - console.warn('findServerIndex: index not found'); + logger.warn('findServerIndex: index not found'); return true; } @@ -214,73 +255,30 @@ export class WGServer { }); await client.lset(WG_SEVER_PATH, index, JSON.stringify({ ...server, peers: updatedPeers })); - await this.storePeers({ id: server.id, confId: server.confId }, publicKey, updatedPeers); + await this.storePeers(publicKey, updatedPeers); if (server.status === 'up') { - await this.stop(serverId); - await this.start(serverId); + await this.server.stop(); + await this.server.start(); } return true; } - private static async getPeerIndex(id: string, publicKey: string): Promise { - const server = await findServer(id); - if (!server) { - console.error('server could not be updated (reason: not exists)'); - return undefined; - } + async getIndex(publicKey: string): Promise { + const server = await this.server.get(); return server.peers.findIndex((p) => p.publicKey === publicKey); } - private static async storePeers( - sd: Pick, - publicKey: string, - peers: Peer[], - ): Promise { - const peerIndex = await this.getPeerIndex(sd.id, publicKey); - if (peerIndex === -1) { - console.warn('WGServer:StorePeers: no peer found'); - return; - } - - const confPath = path.join(WG_PATH, `wg${sd.confId}.conf`); - const conf = fs.readFileSync(confPath, 'utf-8'); - const serverConfStr = conf.includes('[Peer]') ? conf.split('[Peer]')[0] : conf; - - const peersStr = peers.filter((_, i) => i !== peerIndex).join('\n'); - fs.writeFileSync(confPath, `${serverConfStr}\n${peersStr}`, { mode: 0o600 }); - await WGServer.update(sd.id, { confHash: getConfigHash(sd.confId) }); - } - - static async getFreePeerIp(id: string): Promise { - const server = await findServer(id); + async generateConfig(peerId: string): Promise { + const server = await findServer(this.server.id); if (!server) { - console.error('getFreePeerIp: server not found'); - return undefined; - } - const reservedIps = server.peers.map((p) => p.allowedIps); - const ips = reservedIps.map((ip) => ip.split('/')[0]); - const net = server.address.split('/')[0].split('.'); - for (let i = 1; i < 255; i++) { - const ip = `${net[0]}.${net[1]}.${net[2]}.${i}`; - if (!ips.includes(ip) && ip !== server.address.split('/')[0]) { - return ip; - } - } - console.error('getFreePeerIp: no free ip found'); - return undefined; - } - - static async generatePeerConfig(id: string, peerId: string): Promise { - const server = await findServer(id); - if (!server) { - console.error('generatePeerConfig: server not found'); + logger.error('generatePeerConfig: server not found'); return undefined; } const peer = server.peers.find((p) => p.id === peerId); if (!peer) { - console.error('generatePeerConfig: peer not found'); + logger.error('generatePeerConfig: peer not found'); return undefined; } return await getPeerConf({ @@ -290,6 +288,28 @@ export class WGServer { dns: server.dns, }); } + + private async storePeers(publicKey: string, peers: Peer[]): Promise { + const { confId } = await this.server.get(); + + const peerIndex = await this.getIndex(publicKey); + if (peerIndex === -1) { + logger.warn('WGServer:StorePeers: no peer found'); + return; + } + + const confPath = resolveConfigPath(confId); + const conf = fs.readFileSync(confPath, 'utf-8'); + const serverConfStr = conf.includes('[Peer]') ? conf.split('[Peer]')[0] : conf; + + const peersStr = peers.filter((_, i) => i !== peerIndex).join('\n'); + fs.writeFileSync(confPath, `${serverConfStr}\n${peersStr}`, { mode: 0o600 }); + await this.server.update({ confHash: getConfigHash(confId) }); + } +} + +function resolveConfigPath(confId: number): string { + return path.resolve(path.join(WG_PATH, `wg${confId}.conf`)); } /** @@ -301,7 +321,7 @@ async function wgCheckout(configId: number): Promise { } export async function readWgConf(configId: number): Promise { - const confPath = path.join(WG_PATH, `wg${configId}.conf`); + const confPath = resolveConfigPath(configId); const conf = fs.readFileSync(confPath, 'utf-8'); const lines = conf.split('\n'); const server: WgServer = { @@ -392,13 +412,8 @@ export async function readWgConf(configId: number): Promise { * @param configId */ function wgConfExists(configId: number): boolean { - const confPath = path.join(WG_PATH, `wg${configId}.conf`); - try { - fs.accessSync(confPath); - return true; - } catch (e) { - return false; - } + const confPath = resolveConfigPath(configId); + return fsAccess(confPath); } /** @@ -421,7 +436,7 @@ async function syncServers(): Promise { } function wgPeersStr(configId: number): string[] { - const confPath = path.join(WG_PATH, `wg${configId}.conf`); + const confPath = path.resolve(WG_PATH, `wg${configId}.conf`); const conf = fs.readFileSync(confPath, 'utf-8'); const rawPeers = conf.split('[Peer]'); return rawPeers.slice(1).map((p) => `[Peer]\n${p}`); @@ -491,13 +506,14 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise< await client.lpush(WG_SEVER_PATH, JSON.stringify(server)); } - const CONFIG_PATH = path.join(WG_PATH, `wg${confId}.conf`); + const CONFIG_PATH = resolveConfigPath(confId); // save server config to disk fs.writeFileSync(CONFIG_PATH, await genServerConf(server), { mode: 0o600 }); // updating hash of the config - await WGServer.update(uuid, { confHash: getConfigHash(confId) }); + const wg = new WGServer(uuid); + await wg.update({ confHash: getConfigHash(confId) }); // to ensure interface does not exists await Shell.exec(`wg-quick down wg${confId}`, true); @@ -539,15 +555,9 @@ export function getConfigHash(confId: number): string | undefined { return undefined; } - const confPath = path.join(WG_PATH, `wg${confId}.conf`); + const confPath = resolveConfigPath(confId); const conf = fs.readFileSync(confPath, 'utf-8'); - return Hex.stringify(SHA256(conf)); -} - -export async function writeConfigFile(wg: WgServer): Promise { - const CONFIG_PATH = path.join(WG_PATH, `wg${wg.confId}.conf`); - fs.writeFileSync(CONFIG_PATH, await genServerConf(wg), { mode: 0o600 }); - await WGServer.update(wg.id, { confHash: getConfigHash(wg.confId) }); + return sha256(conf); } export function maxConfId(): number { @@ -589,8 +599,8 @@ export async function findServer(id: string | undefined, hash?: string): Promise return id ? servers.find((s) => s.id === id) : hash && isJson(hash) - ? servers.find((s) => JSON.stringify(s) === hash) - : undefined; + ? servers.find((s) => JSON.stringify(s) === hash) + : undefined; } export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: string }> { diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts index b7ee133..19d984f 100644 --- a/web/src/routes/+page.server.ts +++ b/web/src/routes/+page.server.ts @@ -30,7 +30,8 @@ export const actions: Actions = { return error(400, 'Bad Request'); } - await WGServer.update(server.id, { name }); + const wg = new WGServer(server.id); + await wg.update({ name }); return { ok: true }; }, diff --git a/web/src/routes/[serverId]/+page.server.ts b/web/src/routes/[serverId]/+page.server.ts index 12813f2..3b755d4 100644 --- a/web/src/routes/[serverId]/+page.server.ts +++ b/web/src/routes/[serverId]/+page.server.ts @@ -41,7 +41,8 @@ export const actions: Actions = { } try { - await WGServer.updatePeer(server.id, peer.publicKey, { name }); + const wg = new WGServer(server.id); + await wg.peers.update(peer.publicKey, { name }); return { ok: true }; } catch (e) { @@ -63,7 +64,8 @@ export const actions: Actions = { const peerId = (form.get('id') ?? '').toString(); const peer = server.peers.find((p) => p.id === peerId); if (peer) { - await WGServer.removePeer(server.id, peer.publicKey); + const wg = new WGServer(server.id); + await wg.peers.remove(peer.publicKey); } return { ok: true }; @@ -78,7 +80,8 @@ export const actions: Actions = { try { const server = await findServer(serverId ?? ''); if (server) { - await WGServer.remove(server.id); + const wg = new WGServer(server.id); + await wg.remove(); } return { ok: true }; @@ -99,24 +102,26 @@ export const actions: Actions = { const form = await request.formData(); const status = (form.get('state') ?? '').toString(); + const wg = new WGServer(server.id); + try { if (server.status !== status) { switch (status) { case 'start': - await WGServer.start(server.id); + await wg.start(); break; case 'stop': - await WGServer.stop(server.id); + await wg.stop(); break; case 'remove': - await WGServer.remove(server.id); + await wg.remove(); break; case 'restart': - await WGServer.stop(server.id); - await WGServer.start(server.id); + await wg.stop(); + await wg.start(); break; } } @@ -151,7 +156,8 @@ export const actions: Actions = { const peerKeys = await generateWgKey(); - const addedPeer = await WGServer.addPeer(server.id, { + const wg = new WGServer(server.id); + const addedPeer = await wg.peers.add({ id: crypto.randomUUID(), name, allowedIps: freeAddress, diff --git a/web/src/routes/api/health/+server.ts b/web/src/routes/api/health/+server.ts index eaacc5b..5d7d8e8 100644 --- a/web/src/routes/api/health/+server.ts +++ b/web/src/routes/api/health/+server.ts @@ -7,15 +7,11 @@ export const GET: RequestHandler = async () => { const servers = await getServers(); for (const s of servers) { - const HASH = getConfigHash(s.confId); - if (s.confId && HASH && s.confHash === HASH) { - // Skip, due to no changes on the config - continue; - } + const wg = new WGServer(s.id); // Start server if (s.status === 'up') { - await WGServer.start(s.id); + await wg.start(); } } } catch (e) { diff --git a/web/src/routes/login/+page.server.ts b/web/src/routes/login/+page.server.ts index fdffb26..9d9d8cc 100644 --- a/web/src/routes/login/+page.server.ts +++ b/web/src/routes/login/+page.server.ts @@ -4,8 +4,8 @@ import type { PageServerLoad } from './$types'; import { setError, superValidate } from 'sveltekit-superforms/server'; import { formSchema } from './schema'; import { generateToken } from '$lib/auth'; -import 'dotenv/config'; import logger from '$lib/logger'; +import dotenv from 'dotenv'; export const load: PageServerLoad = () => { return { @@ -22,8 +22,10 @@ export const actions: Actions = { return fail(400, { ok: false, message: 'Bad Request', form }); } + dotenv.config(); + const { HASHED_PASSWORD } = process.env; - if (HASHED_PASSWORD) { + if (HASHED_PASSWORD && HASHED_PASSWORD !== '') { const { password } = form.data; const hashed = HASHED_PASSWORD.toLowerCase(); @@ -34,7 +36,7 @@ export const actions: Actions = { } } - if (!HASHED_PASSWORD) { + if (!HASHED_PASSWORD || HASHED_PASSWORD === '') { logger.warn('No password is set!'); }