This commit is contained in:
Shahrad Elahi 2023-11-07 23:43:54 +03:30
parent 98ea76be1a
commit 034465d17e
10 changed files with 360 additions and 49 deletions

View File

@ -41,6 +41,8 @@ export const PeerSchema = z
export type Peer = z.infer<typeof PeerSchema>;
export const WgServerStatusSchema = z.enum(['up', 'down']);
export const WgServerSchema = z
.object({
id: z.string().uuid(),
@ -58,7 +60,7 @@ export const WgServerSchema = z
peers: z.array(PeerSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
status: z.enum(['up', 'down']),
status: WgServerStatusSchema,
})
.merge(WgKeySchema.omit({ preSharedKey: true }));

View File

@ -1,7 +1,8 @@
import fs from 'fs';
import path from 'path';
import deepmerge from 'deepmerge';
import { enc, SHA256 } from 'crypto-js';
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';
@ -57,7 +58,9 @@ export class WGServer {
}
await this.stop(id);
fs.unlinkSync(path.join(WG_PATH, `wg${server.confId}.conf`));
if (wgConfExists(server.confId)) {
fs.unlinkSync(path.join(WG_PATH, `wg${server.confId}.conf`));
}
const index = await findServerIndex(id);
if (typeof index !== 'number') {
@ -538,7 +541,7 @@ export function getConfigHash(confId: number): string | undefined {
const confPath = path.join(WG_PATH, `wg${confId}.conf`);
const conf = fs.readFileSync(confPath, 'utf-8');
return enc.Hex.stringify(SHA256(conf));
return Hex.stringify(SHA256(conf));
}
export async function writeConfigFile(wg: WgServer): Promise<void> {

View File

@ -71,7 +71,8 @@ export const actions: Actions = {
serverId,
};
} catch (e: any) {
return setError(form, e.message);
console.error('Exception:', e);
return setError(form, 'Unhandled Exception');
}
},
};

View File

@ -14,7 +14,6 @@
} from '$lib/components/ui/form';
import { goto } from '$app/navigation';
import { FormItem } from '$lib/components/ui/form/index.js';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { cn } from '$lib/utils';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button';
@ -30,7 +29,6 @@
<DialogHeader>
<DialogTitle>Create Server</DialogTitle>
</DialogHeader>
<SuperDebug data={form} />
<Form
{form}
schema={CreateServerSchema}
@ -43,6 +41,7 @@
loading = true;
},
onError: (e) => {
loading = false;
console.error('Client-side: FormError:', e);
},
onResult: ({ result }) => {

View File

@ -43,15 +43,11 @@
</div>
</div>
<div class={'flex col-span-4 justify-end'}>
<Badge variant={server.status === 'up' ? 'success' : 'destructive'}>
{#if server.status === 'up'}
<span class="hidden md:inline">Online</span>
<span class="inline md:hidden">Up</span>
{:else}
<span class="hidden md:inline">Offline</span>
<span class="inline md:hidden">Down</span>
{/if}
</Badge>
{#if server.status === 'up'}
<Badge variant="success">Online</Badge>
{:else}
<Badge variant="destructive">Offline</Badge>
{/if}
</div>
</div>

View File

@ -1,7 +1,10 @@
import { type Actions, error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { findServer, WGServer } from '$lib/wireguard';
import { findServer, generateWgKey, WGServer } from '$lib/wireguard';
import { NameSchema } from '$lib/wireguard/schema';
import { setError, superValidate } from 'sveltekit-superforms/server';
import { CreatePeerSchema } from './schema';
import { WgServerStatusSchema } from '$lib/typings';
export const load: PageServerLoad = async ({ params }) => {
const { serverId } = params;
@ -17,30 +20,156 @@ export const load: PageServerLoad = async ({ params }) => {
export const actions: Actions = {
rename: async ({ request, params }) => {
const { serverId } = params;
const server = await findServer(serverId ?? '');
if (!server) {
console.error('Server not found');
return error(404, 'Not found');
throw error(404, 'Not found');
}
const form = await request.formData();
const peerId = (form.get('id') ?? '').toString();
const peer = server.peers.find((p) => p.id === peerId);
if (!peer) {
console.error('Peer not found');
return error(404, 'Not found');
throw error(404, 'Not found');
}
const name = (form.get('name') ?? '').toString();
if (!NameSchema.safeParse(name).success) {
console.error('Peer name is invalid');
return error(400, 'Bad Request');
throw error(400, 'Bad Request');
}
await WGServer.updatePeer(server.id, peer.publicKey, { name });
try {
await WGServer.updatePeer(server.id, peer.publicKey, { name });
return { ok: true };
return { ok: true };
} catch (e) {
console.error('Exception:', e);
throw error(500, 'Unhandled Exception');
}
},
remove: async ({ request, params }) => {
const { serverId } = params;
const server = await findServer(serverId ?? '');
if (!server) {
console.error('Server not found');
throw error(404, 'Not found');
}
try {
const form = await request.formData();
const peerId = (form.get('id') ?? '').toString();
const peer = server.peers.find((p) => p.id === peerId);
if (peer) {
await WGServer.removePeer(server.id, peer.publicKey);
}
return { ok: true };
} catch (e) {
console.error('Exception:', e);
throw error(500, 'Unhandled Exception');
}
},
'remove-server': async ({ params }) => {
const { serverId } = params;
try {
const server = await findServer(serverId ?? '');
if (server) {
await WGServer.remove(server.id);
}
return { ok: true };
} catch (e) {
console.error('Exception:', e);
throw error(500, 'Unhandled Exception');
}
},
'change-server-state': async ({ request, params }) => {
const { serverId } = params;
const server = await findServer(serverId ?? '');
if (!server) {
console.error('Server not found');
throw error(404, 'Not found');
}
const form = await request.formData();
const status = (form.get('state') ?? '').toString();
try {
if (server.status !== status) {
switch (status) {
case 'start':
await WGServer.start(server.id);
break;
case 'stop':
await WGServer.stop(server.id);
break;
case 'remove':
await WGServer.remove(server.id);
break;
case 'restart':
await WGServer.stop(server.id);
await WGServer.start(server.id);
break;
}
}
return { ok: true };
} catch (e) {
console.error('Exception:', e);
throw error(500, 'Unhandled Exception');
}
},
create: async (event) => {
const form = await superValidate(event, CreatePeerSchema);
if (!form.valid) {
return setError(form, 'Bad Request');
}
const { serverId } = event.params;
const { name } = form.data;
try {
const server = await findServer(serverId ?? '');
if (!server) {
console.error('Server not found');
return setError(form, 'Server not found');
}
const freeAddress = await WGServer.getFreePeerIp(server.id);
if (!freeAddress) {
console.error(`ERR: ServerId: ${serverId};`, 'No free addresses;');
return setError(form, 'No free addresses');
}
const peerKeys = await generateWgKey();
const addedPeer = await WGServer.addPeer(server.id, {
id: crypto.randomUUID(),
name,
allowedIps: freeAddress,
publicKey: peerKeys.publicKey,
privateKey: peerKeys.privateKey,
preSharedKey: peerKeys.preSharedKey,
persistentKeepalive: 0,
});
if (!addedPeer) {
console.error(`ERR: ServerId: ${serverId};`, 'Failed to add peer;');
return setError(form, 'Failed to add peer');
}
return { ok: true };
} catch (e) {
console.error('Exception:', e);
return setError(form, 'Unhandled Exception');
}
},
};

View File

@ -6,6 +6,11 @@
import { Button } from '$lib/components/ui/button';
import Peer from './Peer.svelte';
import fetchAction from '$lib/fetch-action';
import DetailRow from './DetailRow.svelte';
import { Badge } from '$lib/components/ui/badge';
import { CopiableText } from '$lib/components/copiable-text';
import { MiddleEllipsis } from '$lib/components/middle-ellipsis';
import { goto, invalidateAll } from '$app/navigation';
export let data: PageData;
export let dialogOpen: boolean = false;
@ -47,36 +52,126 @@
}
data.server.peers = data.server.peers.filter((peer) => peer.id !== peerId);
};
const handleChangeState = async (state: string) => {
const resp = await fetchAction({
action: '?/change-server-state',
method: 'POST',
form: {
state,
},
});
if (resp.statusText !== 'OK') {
console.error('err: failed to change server state.');
return;
}
console.log('server state changed!');
if (state === 'remove') {
await goto('/');
return;
}
await invalidateAll();
};
</script>
<CreatePeerDialog serverId={data.server.id} open={dialogOpen} />
<CreatePeerDialog open={dialogOpen} on:close={() => (dialogOpen = false)} />
<Card>Hello there!</Card>
<Card>
<CardHeader>
<CardTitle>Server</CardTitle>
</CardHeader>
<CardContent>
<DetailRow label={'Name'}>
<CopiableText value={data.server.name}>
{data.server.name}
</CopiableText>
</DetailRow>
<DetailRow label={'IP address'}>
<pre> {data.server.address}/24 </pre>
</DetailRow>
<DetailRow label={'Listen Port'}>
<pre> {data.server.listen} </pre>
</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={16} />
</CopiableText>
</DetailRow>
</CardContent>
<CardFooter class="flex flex-wrap items-center gap-2">
{#if data.server.status === 'up'}
<Button
variant="outline"
class="max-md:w-full"
size="sm"
on:click={() => {
handleChangeState('restart');
}}>Restart</Button
>
<Button
variant="destructive"
class="max-md:w-full"
size="sm"
on:click={() => {
handleChangeState('stop');
}}>Stop</Button
>
{:else}
<Button
variant="success"
class="max-md:w-full bg-green-500"
size="sm"
on:click={() => {
handleChangeState('start');
}}>Start</Button
>
<Button
variant="destructive"
class="max-md:w-full"
size="sm"
on:click={() => {
handleChangeState('remove');
}}>Remove</Button
>
{/if}
</CardFooter>
</Card>
<Card>
<CardHeader class="flex flex-row items-center justify-between">
<CardTitle>Clients</CardTitle>
<span> </span>
</CardHeader>
<CardContent>
{#each data.server.peers as peer}
<Peer
{peer}
serverKey={data.server.publicKey}
serverPort={data.server.listen}
serverDNS={data.server.dns}
on:rename={({ detail }) => {
handleRename(peer.id.toString(), detail);
}}
on:remove={() => {
handleRemove(peer.id.toString());
}}
/>
{/each}
</CardContent>
{#if data.server.peers.length > 0}
<CardContent class="space-y-3">
{#each data.server.peers as peer}
<Peer
{peer}
serverKey={data.server.publicKey}
serverPort={data.server.listen}
serverDNS={data.server.dns}
on:rename={({ detail }) => {
handleRename(peer.id.toString(), detail);
}}
on:remove={() => {
handleRemove(peer.id.toString());
}}
/>
{/each}
</CardContent>
<CardFooter>
<Button class="btn btn-primary" on:click={() => (dialogOpen = true)}>Add Client</Button>
<Button on:click={() => (dialogOpen = true)}>Add Client</Button>
</CardFooter>
{:else}
<CardContent>
<div>No Clients!</div>
<Button on:click={() => (dialogOpen = true)}>Add Client</Button>
</CardContent>
{/if}
</Card>

View File

@ -1,13 +1,70 @@
<script lang="ts">
import { Dialog, DialogContent } from '$lib/components/ui/dialog';
import { CreatePeerSchema, type CreatePeerSchemaType } from './schema';
import type { SuperValidated } from 'sveltekit-superforms';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '$lib/components/ui/dialog';
import { Form, FormButton, FormField, FormInput, 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';
export let serverId: string;
export let open: boolean = false;
const dispatch = createEventDispatcher();
export let open = false;
let loading: boolean = false;
let form: SuperValidated<CreatePeerSchemaType>;
const handleSuccess = async () => {
await invalidateAll();
dispatch('close', 'OK');
};
</script>
<Dialog {open}>
<DialogContent>
<h2>Create Peer</h2>
<p>Server Id: {serverId}</p>
<DialogHeader>
<DialogTitle>Create Peer</DialogTitle>
</DialogHeader>
<Form
{form}
schema={CreatePeerSchema}
class="space-y-3.5"
action="?/create"
method={'POST'}
let:config
options={{
onSubmit: (s) => {
loading = true;
},
onError: (e) => {
loading = false;
console.error('Client-side: FormError:', e);
},
onResult: ({ result }) => {
if (result.type === 'success') {
handleSuccess();
} else {
console.error('Server-failure: Result:', result);
}
loading = false;
},
}}
>
<FormField {config} name={'name'}>
<FormItem>
<FormLabel>Name</FormLabel>
<FormInput placeholder={'e.g. Unicorn'} type={'text'} />
<FormValidation />
</FormItem>
</FormField>
<DialogFooter>
<FormButton>
<i class={cn(loading ? 'far fa-arrow-rotate-right animate-spin' : 'far fa-plus', 'mr-2')}></i>
Create
</FormButton>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from '$lib/utils';
export let label: string;
</script>
<div
class={cn(
'flex flex-wrap items-center justify-between gap-2 py-3.5',
'relative overflow-ellipsis',
'leading-none',
'border-b border-gray-200/80 last:border-none',
)}
>
<div class={'flex items-center text-gray-400 text-sm col-span-12 md:col-span-3'}>
{label}
</div>
<div class={'flex items-center gap-x-2 col-span-12 md:col-span-9'}>
<slot />
</div>
</div>

View File

@ -0,0 +1,8 @@
import { NameSchema } from '$lib/wireguard/schema';
import { z } from 'zod';
export const CreatePeerSchema = z.object({
name: NameSchema,
});
export type CreatePeerSchemaType = typeof CreatePeerSchema;