feat: show total net usage and connection mode in server page

This commit is contained in:
Shahrad Elahi 2023-12-21 17:54:10 +03:30
parent 212363c7fa
commit 9dd427f90c
5 changed files with 92 additions and 25 deletions

View File

@ -3,12 +3,12 @@
export let showInHover: boolean = false; export let showInHover: boolean = false;
export let rootClass: string | undefined = undefined; export let rootClass: string | undefined = undefined;
export let value: string; export let value: string | number;
let className: string | undefined = undefined; let className: string | undefined = undefined;
export { className as class }; export { className as class };
const handleCopy = () => { const handleCopy = () => {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value?.toString() || '');
}; };
</script> </script>

View File

@ -11,6 +11,7 @@ export const badgeVariants = tv({
destructive: destructive:
'bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground', 'bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground',
outline: 'text-foreground', outline: 'text-foreground',
tor: 'bg-purple-700 hover:bg-purple-700/80 border-transparent text-white',
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -11,6 +11,7 @@ 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 { getClient } from '$lib/redis';
import { execaCommand } from 'execa';
export class WGServer { export class WGServer {
readonly id: string; readonly id: string;
@ -133,25 +134,41 @@ export class WGServer {
async hasInterface(): Promise<boolean> { async hasInterface(): Promise<boolean> {
const server = await this.get(); const server = await this.get();
return await Network.checkInterfaceExists(`wg${server.confId}`); try {
const res = await execaCommand(`wg show wg${server.confId}`);
return res.stdout.includes('wg');
} catch (e) {
return false;
}
} }
async getUsage(): Promise<WgUsage> { async getUsage(): Promise<WgUsage> {
const server = await this.get(); const server = await this.get();
const hasInterface = await this.hasInterface(); const hasInterface = await this.hasInterface();
const usages: WgUsage = {
total: { rx: 0, tx: 0 },
peers: new Map(),
};
if (!hasInterface) { if (!hasInterface) {
logger.error('GetUsage: interface does not exists'); logger.debug('GetUsage: interface does not exists');
return new Map(); return usages;
} }
const res = await Shell.exec(`wg show wg${server.confId} transfer`); const { stdout, stderr } = await execaCommand(`wg show wg${server.confId} transfer`);
const lines = res.split('\n'); if (stderr) {
logger.warn(`WgServer: GetUsage: ${stderr}`);
return usages;
}
const usages: WgUsage = new Map(); const lines = stdout.split('\n');
for (const line of lines) { for (const line of lines) {
const [peer, rx, tx] = line.split('\t'); const [peer, tx, rx] = line.split('\t');
if (!peer) continue; if (!peer) continue;
usages.set(peer, { rx: Number(rx), tx: Number(tx) }); usages.peers.set(peer, { rx: Number(rx), tx: Number(tx) });
usages.total.rx += Number(rx);
usages.total.tx += Number(tx);
} }
return usages; return usages;
@ -180,7 +197,10 @@ export class WGServer {
} }
} }
export type WgUsage = Map<string, PeerUsage>; export type WgUsage = {
total: PeerUsage;
peers: Map<string, PeerUsage>;
};
export type PeerUsage = { export type PeerUsage = {
rx: number; rx: number;

View File

@ -1,4 +1,4 @@
import { type Actions, error } from '@sveltejs/kit'; import { type Actions, error, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { findServer, generateWgKey, WGServer } from '$lib/wireguard'; import { findServer, generateWgKey, WGServer } from '$lib/wireguard';
import { NameSchema } from '$lib/wireguard/schema'; import { NameSchema } from '$lib/wireguard/schema';
@ -8,10 +8,25 @@ import logger from '$lib/logger';
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
const { serverId } = params; const { serverId } = params;
const server = await findServer(serverId); const exists = await WGServer.exists(serverId ?? '');
if (server) { if (exists) {
return { server }; const wg = new WGServer(serverId);
const server = await wg.get();
if (server.status === 'up') {
const hasInterface = await wg.hasInterface();
if (!hasInterface) {
await wg.start();
}
}
const usage = await wg.getUsage();
return {
server,
usage,
};
} }
throw error(404, 'Not found'); throw error(404, 'Not found');
@ -95,8 +110,8 @@ export const actions: Actions = {
const server = await findServer(serverId ?? ''); const server = await findServer(serverId ?? '');
if (!server) { if (!server) {
console.error('Server not found'); logger.error('Action: ChangeState: Server not found');
throw error(404, 'Not found'); throw redirect(303, '/');
} }
const form = await request.formData(); const form = await request.formData();
@ -128,7 +143,7 @@ export const actions: Actions = {
return { ok: true }; return { ok: true };
} catch (e) { } catch (e) {
console.error('Exception:', e); logger.error('Exception: ChangeState:', e);
throw error(500, 'Unhandled Exception'); throw error(500, 'Unhandled Exception');
} }
}, },

View File

@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { Card } from '$lib/components/ui/card';
import CreatePeerDialog from './CreatePeerDialog.svelte'; import CreatePeerDialog from './CreatePeerDialog.svelte';
import { CardContent, CardFooter, CardHeader, CardTitle } from '$lib/components/ui/card/index.js'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Peer from './Peer.svelte'; import Peer from './Peer.svelte';
import fetchAction from '$lib/fetch-action'; import fetchAction from '$lib/fetch-action';
@ -12,6 +11,8 @@
import { MiddleEllipsis } from '$lib/components/middle-ellipsis'; import { MiddleEllipsis } from '$lib/components/middle-ellipsis';
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { Empty } from '$lib/components/empty'; import { Empty } from '$lib/components/empty';
import prettyBytes from 'pretty-bytes';
import { onDestroy } from 'svelte';
export let data: PageData; export let data: PageData;
@ -57,16 +58,13 @@
const resp = await fetchAction({ const resp = await fetchAction({
action: '?/change-server-state', action: '?/change-server-state',
method: 'POST', method: 'POST',
form: { form: { state },
state,
},
}); });
if (resp.statusText !== 'OK') { if (resp.statusText !== 'OK') {
console.error('err: failed to change server state.'); console.error('error: failed to change server state.');
return; return;
} }
console.log('server state changed!');
if (state === 'remove') { if (state === 'remove') {
await goto('/'); await goto('/');
return; return;
@ -74,6 +72,16 @@
await invalidateAll(); await invalidateAll();
}; };
// revalidate every 2 seconds
const interval = setInterval(() => {
invalidateAll();
}, 2000);
onDestroy(() => {
clearInterval(interval);
});
</script> </script>
<Card> <Card>
@ -87,15 +95,38 @@
{data.server.name} {data.server.name}
</CopiableText> </CopiableText>
</DetailRow> </DetailRow>
{#if data.server.tor}
<DetailRow label={'Mode'}>
<Badge variant="tor">Tor</Badge>
</DetailRow>
{/if}
<DetailRow label={'IP address'}> <DetailRow label={'IP address'}>
<pre> {data.server.address}/24 </pre> <pre> {data.server.address}/24 </pre>
</DetailRow> </DetailRow>
<DetailRow label={'Listen Port'}> <DetailRow label={'Listen Port'}>
<pre> {data.server.listen} </pre> <pre> {data.server.listen} </pre>
</DetailRow> </DetailRow>
<DetailRow label={'Total Usage'}>
<div class="flex items-center gap-3 text-sm">
<div class="flex items-center gap-x-1.5">
<i class="fas fa-arrow-up text-gray-500"></i>
<span>{prettyBytes(data.usage.total.tx)}</span>
</div>
<div class="flex items-center gap-x-1.5">
<i class="fas fa-arrow-down text-gray-500"></i>
<span>{prettyBytes(data.usage.total.rx)}</span>
</div>
</div>
</DetailRow>
<DetailRow label={'Status'}> <DetailRow label={'Status'}>
<Badge variant={data.server.status === 'up' ? 'success' : 'destructive'} /> <Badge variant={data.server.status === 'up' ? 'success' : 'destructive'} />
</DetailRow> </DetailRow>
<DetailRow label={'Public Key'}> <DetailRow label={'Public Key'}>
<CopiableText value={data.server.publicKey}> <CopiableText value={data.server.publicKey}>
<MiddleEllipsis content={data.server.publicKey} maxLength={12} /> <MiddleEllipsis content={data.server.publicKey} maxLength={12} />