From 0dc9e89b6cdb3ec30f87617d6ecb3d97a4be5d25 Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Fri, 22 Dec 2023 00:34:07 +0330 Subject: [PATCH] fix --- web/.mocharc.json | 2 +- web/mocha.setup.js | 1 + web/package.json | 6 +- web/pnpm-lock.yaml | 29 ++++++++ web/src/lib/logger.ts | 15 ++--- web/src/lib/network.ts | 18 ++--- web/src/lib/redis.ts | 2 +- web/src/lib/wireguard/index.ts | 26 +++++--- web/src/routes/+layout.svelte | 2 + web/src/routes/+page.server.ts | 10 +-- web/src/routes/CreateServerDialog.svelte | 21 +++--- web/src/routes/[serverId]/+page.server.ts | 66 ++++++++++--------- .../routes/[serverId]/CreatePeerDialog.svelte | 15 +++-- web/src/routes/api/host/+server.ts | 2 +- web/tests/network.test.ts | 18 +++++ web/tests/wireguard.test.ts | 8 +++ 16 files changed, 156 insertions(+), 85 deletions(-) create mode 100644 web/mocha.setup.js create mode 100644 web/tests/network.test.ts create mode 100644 web/tests/wireguard.test.ts diff --git a/web/.mocharc.json b/web/.mocharc.json index 56e5183..c4db8a4 100644 --- a/web/.mocharc.json +++ b/web/.mocharc.json @@ -1,5 +1,5 @@ { "$schema": "https://json.schemastore.org/mocharc.json", - "require": ["tsx", "chai/register-expect"], + "require": ["tsx", "chai/register-expect", "mocha.setup.js"], "timeout": 10000 } diff --git a/web/mocha.setup.js b/web/mocha.setup.js new file mode 100644 index 0000000..f7534b3 --- /dev/null +++ b/web/mocha.setup.js @@ -0,0 +1 @@ +process.env.NODE_ENV = 'test'; diff --git a/web/package.json b/web/package.json index eec9385..874a44c 100644 --- a/web/package.json +++ b/web/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "vite dev", + "dev": "NODE_ENV=development vite dev", "build": "NODE_ENV=build vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -11,7 +11,7 @@ "test": "mocha", "lint": "prettier --check .", "format": "prettier --write .", - "start": "node ./build/index.js" + "start": "NODE_ENV=production node ./build/index.js" }, "devDependencies": { "@sveltejs/adapter-node": "^2.0.1", @@ -50,10 +50,12 @@ "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.2", "lucide-svelte": "^0.294.0", + "node-netkit": "0.1.0-canary.1", "pino": "^8.17.1", "pino-pretty": "^10.3.0", "pretty-bytes": "^6.1.1", "qrcode": "^1.5.3", + "svelte-french-toast": "^1.2.0", "tailwind-merge": "^2.1.0", "tailwind-variants": "^0.1.18" } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 00e89e5..bccf0e0 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: lucide-svelte: specifier: ^0.294.0 version: 0.294.0(svelte@4.2.8) + node-netkit: + specifier: 0.1.0-canary.1 + version: 0.1.0-canary.1 pino: specifier: ^8.17.1 version: 8.17.1 @@ -44,6 +47,9 @@ dependencies: qrcode: specifier: ^1.5.3 version: 1.5.3 + svelte-french-toast: + specifier: ^1.2.0 + version: 1.2.0(svelte@4.2.8) tailwind-merge: specifier: ^2.1.0 version: 2.1.0 @@ -2068,6 +2074,12 @@ packages: hasBin: true dev: false + /node-netkit@0.1.0-canary.1: + resolution: {integrity: sha512-RjSQt0LBeWPkSlQ8anel3vplQONVahdI5nkA6/PWwA9ZZIY/g/sFB0o62D6PYU264XWssEJFPkRHIpoGBfNBIA==} + dependencies: + execa: 8.0.1 + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true @@ -2696,6 +2708,15 @@ packages: - sugarss dev: true + /svelte-french-toast@1.2.0(svelte@4.2.8): + resolution: {integrity: sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==} + peerDependencies: + svelte: ^3.57.0 || ^4.0.0 + dependencies: + svelte: 4.2.8 + svelte-writable-derived: 3.1.0(svelte@4.2.8) + dev: false + /svelte-hmr@0.15.3(svelte@4.2.8): resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} engines: {node: ^12.20 || ^14.13.1 || >= 16} @@ -2753,6 +2774,14 @@ packages: typescript: 5.3.3 dev: true + /svelte-writable-derived@3.1.0(svelte@4.2.8): + resolution: {integrity: sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==} + peerDependencies: + svelte: ^3.2.1 || ^4.0.0-next.1 + dependencies: + svelte: 4.2.8 + dev: false + /svelte@4.2.8: resolution: {integrity: sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA==} engines: {node: '>=16'} diff --git a/web/src/lib/logger.ts b/web/src/lib/logger.ts index 3d85210..1ef7078 100644 --- a/web/src/lib/logger.ts +++ b/web/src/lib/logger.ts @@ -19,13 +19,8 @@ let logger: Logger = pino( pino.multistream([prettyStream]), ); -fsTouch(LOG_FILE_PATH) - .then(() => fsAccess(LOG_FILE_PATH)) - .then((ok) => { - if (!ok) { - logger.warn('Log file is not accessible'); - return; - } +if (fsAccess(LOG_FILE_PATH)) { + fsTouch(LOG_FILE_PATH).then(() => { logger = pino( { level: LOG_LEVEL, @@ -37,7 +32,9 @@ fsTouch(LOG_FILE_PATH) }), ]), ); - }) - .catch(console.error); + }); +} else { + logger.warn('Log file is not accessible'); +} export default logger; diff --git a/web/src/lib/network.ts b/web/src/lib/network.ts index 85ed229..8882023 100644 --- a/web/src/lib/network.ts +++ b/web/src/lib/network.ts @@ -1,20 +1,21 @@ -import { execaCommand } from 'execa'; +import { execa } from 'execa'; import logger from '$lib/logger'; +import { ip } from 'node-netkit'; export default class Network { public static async dropInterface(inet: string) { - await execaCommand(`ip link delete dev ${inet}`); + await execa(`ip link delete dev ${inet}`, { shell: true }); } public static async defaultInterface(): Promise { - const { stdout: o } = await execaCommand(`ip route list default | awk '{print $5}'`); - return o.trim(); + const route = await ip.route.defaultRoute(); + if (!route) throw new Error('No default route found'); + return route.dev; } public static async interfaceExists(inet: string): Promise { try { - const { stdout: o } = await execaCommand(`ip link show | grep ${inet}`); - console.log(o); + const { stdout: o } = await execa(`ip link show | grep ${inet}`, { shell: true }); return o.trim() !== ''; } catch (e) { logger.debug('Interface does not exist:', inet); @@ -24,12 +25,13 @@ export default class Network { public static async inUsePorts(): Promise { const ports = []; - const { stdout: output } = await execaCommand( + const { stdout: output } = await execa( `netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`, + { shell: true }, ); for (const line of output.split('\n')) { const clean = Number(line.trim()); - if (!isNaN(clean)) ports.push(clean); + if (!isNaN(clean) && clean !== 0) ports.push(clean); } return ports; diff --git a/web/src/lib/redis.ts b/web/src/lib/redis.ts index 78155fc..72991fc 100644 --- a/web/src/lib/redis.ts +++ b/web/src/lib/redis.ts @@ -16,7 +16,7 @@ export function setClient(redis: RedisClient): void { client = redis; } -if (process.env.NODE_ENV !== 'build') { +if (process.env.NODE_ENV && ['development', 'production'].includes(process.env.NODE_ENV)) { setClient( new Redis({ port: 6479, diff --git a/web/src/lib/wireguard/index.ts b/web/src/lib/wireguard/index.ts index 8a7d3e6..ebde1d2 100644 --- a/web/src/lib/wireguard/index.ts +++ b/web/src/lib/wireguard/index.ts @@ -10,7 +10,7 @@ import logger from '$lib/logger'; import { sha256 } from '$lib/hash'; import { fsAccess } from '$lib/fs-extra'; import { getClient } from '$lib/redis'; -import { execaCommand } from 'execa'; +import { execa } from 'execa'; export class WGServer { readonly id: string; @@ -51,7 +51,7 @@ export class WGServer { const server = await this.get(); if (await Network.interfaceExists(`wg${server.confId}`)) { - await execaCommand(`wg-quick down wg${server.confId}`); + await execa(`wg-quick down wg${server.confId}`, { shell: true }); } await this.update({ status: 'down' }); @@ -70,10 +70,10 @@ export class WGServer { logger.debug('WGServer:Start: isAlreadyUp:', isAlreadyUp); if (isAlreadyUp) { logger.debug('WGServer:Start: interface already up... taking down'); - await execaCommand(`wg-quick down wg${server.confId}`); + await execa(`wg-quick down wg${server.confId}`, { shell: true }); } - await execaCommand(`wg-quick up wg${server.confId}`); + await execa(`wg-quick up wg${server.confId}`, { shell: true }); await this.update({ status: 'up' }); return true; @@ -137,7 +137,7 @@ export class WGServer { async isUp(): Promise { const server = await this.get(); try { - const res = await execaCommand(`wg show wg${server.confId}`); + const res = await execa(`wg show wg${server.confId}`, { shell: true }); return res.stdout.includes('wg'); } catch (e) { return false; @@ -158,7 +158,9 @@ export class WGServer { return usages; } - const { stdout, stderr } = await execaCommand(`wg show wg${server.confId} transfer`); + const { stdout, stderr } = await execa(`wg show wg${server.confId} transfer`, { + shell: true, + }); if (stderr) { logger.warn(`WgServer: GetUsage: ${stderr}`); return usages; @@ -502,9 +504,11 @@ function wgPeersStr(configId: number): string[] { } export async function generateWgKey(): Promise { - const { stdout: privateKey } = await execaCommand('wg genkey'); - const { stdout: publicKey } = await execaCommand(`echo ${privateKey} | wg pubkey`); - const { stdout: preSharedKey } = await execaCommand('wg genkey'); + const { stdout: privateKey } = await execa('wg genkey', { shell: true }); + const { stdout: publicKey } = await execa(`echo ${privateKey} | wg pubkey`, { + shell: true, + }); + const { stdout: preSharedKey } = await execa('wg genkey', { shell: true }); return { privateKey, publicKey, preSharedKey }; } @@ -673,7 +677,9 @@ export async function findServer( export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: string }> { const inet = await Network.defaultInterface(); - const { stdout: inet_address } = await execaCommand(`hostname -i | awk '{print $1}'`); + const { stdout: inet_address } = await execa(`hostname -i | awk '{print $1}'`, { + shell: true, + }); const source = `${s.address}/24`; const wg_inet = `wg${s.confId}`; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index cbd3011..556c87d 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -1,6 +1,8 @@ + diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts index 7af1dc1..a53a127 100644 --- a/web/src/routes/+page.server.ts +++ b/web/src/routes/+page.server.ts @@ -28,12 +28,12 @@ export const actions: Actions = { const server = await findServer(serverId ?? ''); if (!server) { - logger.error('Server not found'); + logger.error('Actions: RenameServer: Server not found'); return error(404, 'Not found'); } if (!NameSchema.safeParse(name).success) { - logger.error('Peer name is invalid'); + logger.error('Actions: RenameServer: Server name is invalid'); return error(400, 'Bad Request'); } @@ -69,11 +69,11 @@ export const actions: Actions = { }); return { - ok: true, + form, serverId, }; - } catch (e: any) { - logger.error('Exception:', e); + } catch (e) { + logger.error(e); return setError(form, 'Unhandled Exception'); } }, diff --git a/web/src/routes/CreateServerDialog.svelte b/web/src/routes/CreateServerDialog.svelte index ffad8d4..c37b7bc 100644 --- a/web/src/routes/CreateServerDialog.svelte +++ b/web/src/routes/CreateServerDialog.svelte @@ -19,22 +19,19 @@ FormSwitch, FormValidation, } from '$lib/components/ui/form'; - import { goto } from '$app/navigation'; import { FormItem } from '$lib/components/ui/form/index.js'; import { cn } from '$lib/utils'; - import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, - } from '$lib/components/ui/collapsible'; + import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible'; import { Button } from '$lib/components/ui/button'; + import toast from 'svelte-french-toast'; let loading: boolean = false; + let dialogOpen = false; let form: SuperValidated; - + @@ -50,7 +47,7 @@ method={'POST'} let:config options={{ - onSubmit: (s) => { + onSubmit: () => { loading = true; }, onError: (e) => { @@ -60,9 +57,11 @@ onResult: ({ result }) => { loading = false; if (result.type === 'success') { - goto(`/${result.data.serverId}`); + dialogOpen = false; + toast.success('Server created successfully!'); } else { console.error('Server-failure: Result:', result); + toast.error('Server failed to create.'); } }, }} @@ -89,7 +88,7 @@ Port This is the port that the WireGuard server will listen on.This is the port that the WireGuard server will listen on. @@ -119,7 +118,7 @@ DNS Optional. This is the DNS server that will be pushed to clients.Optional. This is the DNS server that will be pushed to clients. diff --git a/web/src/routes/[serverId]/+page.server.ts b/web/src/routes/[serverId]/+page.server.ts index bfba3d9..82b3bb2 100644 --- a/web/src/routes/[serverId]/+page.server.ts +++ b/web/src/routes/[serverId]/+page.server.ts @@ -10,26 +10,28 @@ export const load: PageServerLoad = async ({ params }) => { const { serverId } = params; const exists = await WGServer.exists(serverId ?? ''); - if (exists) { - const wg = new WGServer(serverId); - const server = await wg.get(); - - if (server.status === 'up') { - const hasInterface = await wg.isUp(); - if (!hasInterface) { - await wg.start(); - } - } - - const usage = await wg.getUsage(); - - return { - server, - usage, - }; + if (!exists) { + logger.warn(`Server not found. Redirecting to home page. ServerId: ${serverId}`); + throw redirect(303, '/'); } - throw error(404, 'Not found'); + const wg = new WGServer(serverId); + const server = await wg.get(); + + if (server.status === 'up') { + const hasInterface = await wg.isUp(); + if (!hasInterface) { + logger.debug(`Interface not found. Starting WireGuard. ServerId: ${serverId}`); + await wg.start(); + } + } + + const usage = await wg.getUsage(); + + return { + server, + usage, + }; }; export const actions: Actions = { @@ -98,19 +100,19 @@ export const actions: Actions = { const wg = new WGServer(server.id); await wg.remove(); } - - return { ok: true }; } catch (e) { - console.error('Exception:', e); + logger.error(e); throw error(500, 'Unhandled Exception'); } + + return redirect(303, '/'); }, 'change-server-state': async ({ request, params }) => { const { serverId } = params; const server = await findServer(serverId ?? ''); if (!server) { - logger.error('Action: ChangeState: Server not found'); + logger.warn(`Action: ChangeState: Server not found. ServerId: ${serverId}`); throw redirect(303, '/'); } @@ -140,19 +142,20 @@ export const actions: Actions = { break; } } - - return { ok: true }; } catch (e) { logger.error({ - message: 'Exception: ChangeState', + message: `Exception: ChangeState. ServerId: ${serverId}`, exception: e, }); throw error(500, 'Unhandled Exception'); } + + return { ok: true }; }, create: async (event) => { const form = await superValidate(event, CreatePeerSchema); if (!form.valid) { + logger.warn('CreatePeer: Bad Request: failed to validate form'); return setError(form, 'Bad Request'); } @@ -162,13 +165,13 @@ export const actions: Actions = { try { const server = await findServer(serverId ?? ''); if (!server) { - console.error('Server not found'); + logger.error(`Server not found. ServerId: ${serverId}`); return setError(form, 'Server not found'); } const freeAddress = await WGServer.getFreePeerIp(server.id); if (!freeAddress) { - console.error(`ERR: ServerId: ${serverId};`, 'No free addresses;'); + logger.error(`No free addresses. ServerId: ${serverId}`); return setError(form, 'No free addresses'); } @@ -186,13 +189,16 @@ export const actions: Actions = { }); if (!addedPeer) { - console.error(`ERR: ServerId: ${serverId};`, 'Failed to add peer;'); + logger.error(`Failed to add peer. ServerId: ${serverId}`); return setError(form, 'Failed to add peer'); } - return { ok: true }; + return { form }; } catch (e) { - console.error('Exception:', e); + logger.error({ + message: `Exception: CreatePeer. ServerId: ${serverId}`, + exception: e, + }); return setError(form, 'Unhandled Exception'); } }, diff --git a/web/src/routes/[serverId]/CreatePeerDialog.svelte b/web/src/routes/[serverId]/CreatePeerDialog.svelte index 547e6d4..bc80b0f 100644 --- a/web/src/routes/[serverId]/CreatePeerDialog.svelte +++ b/web/src/routes/[serverId]/CreatePeerDialog.svelte @@ -14,27 +14,27 @@ FormButton, FormField, FormInput, + FormItem, FormLabel, FormValidation, } from '$lib/components/ui/form'; - import { FormItem } from '$lib/components/ui/form/index.js'; import { cn } from '$lib/utils'; import { invalidateAll } from '$app/navigation'; - import { createEventDispatcher } from 'svelte'; - - const dispatch = createEventDispatcher(); + import toast from 'svelte-french-toast'; let loading: boolean = false; + let dialogOpen = false; let form: SuperValidated; const handleSuccess = async () => { await invalidateAll(); - dispatch('close', 'OK'); + toast.success('Peer created!'); + dialogOpen = false; }; - + @@ -50,7 +50,7 @@ method={'POST'} let:config options={{ - onSubmit: (s) => { + onSubmit: () => { loading = true; }, onError: (e) => { @@ -61,6 +61,7 @@ if (result.type === 'success') { handleSuccess(); } else { + toast.error('Failed to create peer'); console.error('Server-failure: Result:', result); } loading = false; diff --git a/web/src/routes/api/host/+server.ts b/web/src/routes/api/host/+server.ts index 05805f4..320fe4e 100644 --- a/web/src/routes/api/host/+server.ts +++ b/web/src/routes/api/host/+server.ts @@ -8,7 +8,7 @@ export const GET: RequestHandler = async () => { // if the host is not set, then we are using the server's public IP if (!WG_HOST) { - const { stdout: resp } = await execaCommand('curl -s ifconfig.me'); + const { stdout: resp } = await execa('curl -s ifconfig.me', { shell: true }); WG_HOST = resp.trim(); } diff --git a/web/tests/network.test.ts b/web/tests/network.test.ts new file mode 100644 index 0000000..1bb57ca --- /dev/null +++ b/web/tests/network.test.ts @@ -0,0 +1,18 @@ +import Network from '$lib/network'; + +describe('Network', () => { + it('should return default interface', async () => { + const inet = await Network.defaultInterface(); + console.log(inet); + }); + + it('should return in use ports', async () => { + const ports = await Network.inUsePorts(); + console.log(ports); + }); + + it('should check interface exists', async () => { + const exists = await Network.interfaceExists('lo'); + console.log(exists); + }); +}); diff --git a/web/tests/wireguard.test.ts b/web/tests/wireguard.test.ts new file mode 100644 index 0000000..5907c3d --- /dev/null +++ b/web/tests/wireguard.test.ts @@ -0,0 +1,8 @@ +import { generateWgKey } from '$lib/wireguard'; + +describe('Keys', () => { + it('should generate a key', async () => { + const keys = await generateWgKey(); + console.log(keys); + }); +});