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 rootClass: string | undefined = undefined;
export let value: string;
export let value: string | number;
let className: string | undefined = undefined;
export { className as class };
const handleCopy = () => {
navigator.clipboard.writeText(value);
navigator.clipboard.writeText(value?.toString() || '');
};
</script>

View File

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

View File

@ -11,6 +11,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';
export class WGServer {
readonly id: string;
@ -133,25 +134,41 @@ export class WGServer {
async hasInterface(): Promise<boolean> {
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> {
const server = await this.get();
const hasInterface = await this.hasInterface();
const usages: WgUsage = {
total: { rx: 0, tx: 0 },
peers: new Map(),
};
if (!hasInterface) {
logger.error('GetUsage: interface does not exists');
return new Map();
logger.debug('GetUsage: interface does not exists');
return usages;
}
const res = await Shell.exec(`wg show wg${server.confId} transfer`);
const lines = res.split('\n');
const { stdout, stderr } = await execaCommand(`wg show wg${server.confId} transfer`);
if (stderr) {
logger.warn(`WgServer: GetUsage: ${stderr}`);
return usages;
}
const usages: WgUsage = new Map();
const lines = stdout.split('\n');
for (const line of lines) {
const [peer, rx, tx] = line.split('\t');
const [peer, tx, rx] = line.split('\t');
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;
@ -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 = {
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 { findServer, generateWgKey, WGServer } from '$lib/wireguard';
import { NameSchema } from '$lib/wireguard/schema';
@ -8,10 +8,25 @@ import logger from '$lib/logger';
export const load: PageServerLoad = async ({ params }) => {
const { serverId } = params;
const server = await findServer(serverId);
const exists = await WGServer.exists(serverId ?? '');
if (server) {
return { server };
if (exists) {
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');
@ -95,8 +110,8 @@ export const actions: Actions = {
const server = await findServer(serverId ?? '');
if (!server) {
console.error('Server not found');
throw error(404, 'Not found');
logger.error('Action: ChangeState: Server not found');
throw redirect(303, '/');
}
const form = await request.formData();
@ -128,7 +143,7 @@ export const actions: Actions = {
return { ok: true };
} catch (e) {
console.error('Exception:', e);
logger.error('Exception: ChangeState:', e);
throw error(500, 'Unhandled Exception');
}
},

View File

@ -1,8 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import { Card } from '$lib/components/ui/card';
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 Peer from './Peer.svelte';
import fetchAction from '$lib/fetch-action';
@ -12,6 +11,8 @@
import { MiddleEllipsis } from '$lib/components/middle-ellipsis';
import { goto, invalidateAll } from '$app/navigation';
import { Empty } from '$lib/components/empty';
import prettyBytes from 'pretty-bytes';
import { onDestroy } from 'svelte';
export let data: PageData;
@ -57,16 +58,13 @@
const resp = await fetchAction({
action: '?/change-server-state',
method: 'POST',
form: {
state,
},
form: { state },
});
if (resp.statusText !== 'OK') {
console.error('err: failed to change server state.');
console.error('error: failed to change server state.');
return;
}
console.log('server state changed!');
if (state === 'remove') {
await goto('/');
return;
@ -74,6 +72,16 @@
await invalidateAll();
};
// revalidate every 2 seconds
const interval = setInterval(() => {
invalidateAll();
}, 2000);
onDestroy(() => {
clearInterval(interval);
});
</script>
<Card>
@ -87,15 +95,38 @@
{data.server.name}
</CopiableText>
</DetailRow>
{#if data.server.tor}
<DetailRow label={'Mode'}>
<Badge variant="tor">Tor</Badge>
</DetailRow>
{/if}
<DetailRow label={'IP address'}>
<pre> {data.server.address}/24 </pre>
</DetailRow>
<DetailRow label={'Listen Port'}>
<pre> {data.server.listen} </pre>
</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'}>
<Badge variant={data.server.status === 'up' ? 'success' : 'destructive'} />
</DetailRow>
<DetailRow label={'Public Key'}>
<CopiableText value={data.server.publicKey}>
<MiddleEllipsis content={data.server.publicKey} maxLength={12} />