This commit is contained in:
Shahrad Elahi 2023-12-22 00:34:07 +03:30
parent ac5275168c
commit 0dc9e89b6c
16 changed files with 156 additions and 85 deletions

View File

@ -1,5 +1,5 @@
{ {
"$schema": "https://json.schemastore.org/mocharc.json", "$schema": "https://json.schemastore.org/mocharc.json",
"require": ["tsx", "chai/register-expect"], "require": ["tsx", "chai/register-expect", "mocha.setup.js"],
"timeout": 10000 "timeout": 10000
} }

1
web/mocha.setup.js Normal file
View File

@ -0,0 +1 @@
process.env.NODE_ENV = 'test';

View File

@ -3,7 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "NODE_ENV=development vite dev",
"build": "NODE_ENV=build vite build", "build": "NODE_ENV=build vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@ -11,7 +11,7 @@
"test": "mocha", "test": "mocha",
"lint": "prettier --check .", "lint": "prettier --check .",
"format": "prettier --write .", "format": "prettier --write .",
"start": "node ./build/index.js" "start": "NODE_ENV=production node ./build/index.js"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "^2.0.1", "@sveltejs/adapter-node": "^2.0.1",
@ -50,10 +50,12 @@
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.294.0", "lucide-svelte": "^0.294.0",
"node-netkit": "0.1.0-canary.1",
"pino": "^8.17.1", "pino": "^8.17.1",
"pino-pretty": "^10.3.0", "pino-pretty": "^10.3.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"svelte-french-toast": "^1.2.0",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.1.0",
"tailwind-variants": "^0.1.18" "tailwind-variants": "^0.1.18"
} }

View File

@ -32,6 +32,9 @@ dependencies:
lucide-svelte: lucide-svelte:
specifier: ^0.294.0 specifier: ^0.294.0
version: 0.294.0(svelte@4.2.8) version: 0.294.0(svelte@4.2.8)
node-netkit:
specifier: 0.1.0-canary.1
version: 0.1.0-canary.1
pino: pino:
specifier: ^8.17.1 specifier: ^8.17.1
version: 8.17.1 version: 8.17.1
@ -44,6 +47,9 @@ dependencies:
qrcode: qrcode:
specifier: ^1.5.3 specifier: ^1.5.3
version: 1.5.3 version: 1.5.3
svelte-french-toast:
specifier: ^1.2.0
version: 1.2.0(svelte@4.2.8)
tailwind-merge: tailwind-merge:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
@ -2068,6 +2074,12 @@ packages:
hasBin: true hasBin: true
dev: false 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: /node-releases@2.0.13:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: true dev: true
@ -2696,6 +2708,15 @@ packages:
- sugarss - sugarss
dev: true 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): /svelte-hmr@0.15.3(svelte@4.2.8):
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
engines: {node: ^12.20 || ^14.13.1 || >= 16} engines: {node: ^12.20 || ^14.13.1 || >= 16}
@ -2753,6 +2774,14 @@ packages:
typescript: 5.3.3 typescript: 5.3.3
dev: true 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: /svelte@4.2.8:
resolution: {integrity: sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA==} resolution: {integrity: sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA==}
engines: {node: '>=16'} engines: {node: '>=16'}

View File

@ -19,13 +19,8 @@ let logger: Logger = pino(
pino.multistream([prettyStream]), pino.multistream([prettyStream]),
); );
fsTouch(LOG_FILE_PATH) if (fsAccess(LOG_FILE_PATH)) {
.then(() => fsAccess(LOG_FILE_PATH)) fsTouch(LOG_FILE_PATH).then(() => {
.then((ok) => {
if (!ok) {
logger.warn('Log file is not accessible');
return;
}
logger = pino( logger = pino(
{ {
level: LOG_LEVEL, 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; export default logger;

View File

@ -1,20 +1,21 @@
import { execaCommand } from 'execa'; import { execa } from 'execa';
import logger from '$lib/logger'; import logger from '$lib/logger';
import { ip } from 'node-netkit';
export default class Network { export default class Network {
public static async dropInterface(inet: string) { 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<string> { public static async defaultInterface(): Promise<string> {
const { stdout: o } = await execaCommand(`ip route list default | awk '{print $5}'`); const route = await ip.route.defaultRoute();
return o.trim(); if (!route) throw new Error('No default route found');
return route.dev;
} }
public static async interfaceExists(inet: string): Promise<boolean> { public static async interfaceExists(inet: string): Promise<boolean> {
try { try {
const { stdout: o } = await execaCommand(`ip link show | grep ${inet}`); const { stdout: o } = await execa(`ip link show | grep ${inet}`, { shell: true });
console.log(o);
return o.trim() !== ''; return o.trim() !== '';
} catch (e) { } catch (e) {
logger.debug('Interface does not exist:', inet); logger.debug('Interface does not exist:', inet);
@ -24,12 +25,13 @@ export default class Network {
public static async inUsePorts(): Promise<number[]> { public static async inUsePorts(): Promise<number[]> {
const ports = []; const ports = [];
const { stdout: output } = await execaCommand( const { stdout: output } = await execa(
`netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`, `netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`,
{ shell: true },
); );
for (const line of output.split('\n')) { for (const line of output.split('\n')) {
const clean = Number(line.trim()); const clean = Number(line.trim());
if (!isNaN(clean)) ports.push(clean); if (!isNaN(clean) && clean !== 0) ports.push(clean);
} }
return ports; return ports;

View File

@ -16,7 +16,7 @@ export function setClient(redis: RedisClient): void {
client = redis; client = redis;
} }
if (process.env.NODE_ENV !== 'build') { if (process.env.NODE_ENV && ['development', 'production'].includes(process.env.NODE_ENV)) {
setClient( setClient(
new Redis({ new Redis({
port: 6479, port: 6479,

View File

@ -10,7 +10,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'; import { execa } from 'execa';
export class WGServer { export class WGServer {
readonly id: string; readonly id: string;
@ -51,7 +51,7 @@ export class WGServer {
const server = await this.get(); const server = await this.get();
if (await Network.interfaceExists(`wg${server.confId}`)) { 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' }); await this.update({ status: 'down' });
@ -70,10 +70,10 @@ export class WGServer {
logger.debug('WGServer:Start: isAlreadyUp:', isAlreadyUp); logger.debug('WGServer:Start: isAlreadyUp:', isAlreadyUp);
if (isAlreadyUp) { if (isAlreadyUp) {
logger.debug('WGServer:Start: interface already up... taking down'); 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' }); await this.update({ status: 'up' });
return true; return true;
@ -137,7 +137,7 @@ export class WGServer {
async isUp(): Promise<boolean> { async isUp(): Promise<boolean> {
const server = await this.get(); const server = await this.get();
try { 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'); return res.stdout.includes('wg');
} catch (e) { } catch (e) {
return false; return false;
@ -158,7 +158,9 @@ export class WGServer {
return usages; 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) { if (stderr) {
logger.warn(`WgServer: GetUsage: ${stderr}`); logger.warn(`WgServer: GetUsage: ${stderr}`);
return usages; return usages;
@ -502,9 +504,11 @@ function wgPeersStr(configId: number): string[] {
} }
export async function generateWgKey(): Promise<WgKey> { export async function generateWgKey(): Promise<WgKey> {
const { stdout: privateKey } = await execaCommand('wg genkey'); const { stdout: privateKey } = await execa('wg genkey', { shell: true });
const { stdout: publicKey } = await execaCommand(`echo ${privateKey} | wg pubkey`); const { stdout: publicKey } = await execa(`echo ${privateKey} | wg pubkey`, {
const { stdout: preSharedKey } = await execaCommand('wg genkey'); shell: true,
});
const { stdout: preSharedKey } = await execa('wg genkey', { shell: true });
return { privateKey, publicKey, preSharedKey }; return { privateKey, publicKey, preSharedKey };
} }
@ -673,7 +677,9 @@ export async function findServer(
export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: string }> { export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: string }> {
const inet = await Network.defaultInterface(); 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 source = `${s.address}/24`;
const wg_inet = `wg${s.confId}`; const wg_inet = `wg${s.confId}`;

View File

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
import '$lib/assets/fontawesome/index.css'; import '$lib/assets/fontawesome/index.css';
import { Toaster } from 'svelte-french-toast';
</script> </script>
<slot /> <slot />
<Toaster />

View File

@ -28,12 +28,12 @@ export const actions: Actions = {
const server = await findServer(serverId ?? ''); const server = await findServer(serverId ?? '');
if (!server) { if (!server) {
logger.error('Server not found'); logger.error('Actions: RenameServer: Server not found');
return error(404, 'Not found'); return error(404, 'Not found');
} }
if (!NameSchema.safeParse(name).success) { if (!NameSchema.safeParse(name).success) {
logger.error('Peer name is invalid'); logger.error('Actions: RenameServer: Server name is invalid');
return error(400, 'Bad Request'); return error(400, 'Bad Request');
} }
@ -69,11 +69,11 @@ export const actions: Actions = {
}); });
return { return {
ok: true, form,
serverId, serverId,
}; };
} catch (e: any) { } catch (e) {
logger.error('Exception:', e); logger.error(e);
return setError(form, 'Unhandled Exception'); return setError(form, 'Unhandled Exception');
} }
}, },

View File

@ -19,22 +19,19 @@
FormSwitch, FormSwitch,
FormValidation, FormValidation,
} from '$lib/components/ui/form'; } from '$lib/components/ui/form';
import { goto } from '$app/navigation';
import { FormItem } from '$lib/components/ui/form/index.js'; import { FormItem } from '$lib/components/ui/form/index.js';
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
import { import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible';
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import toast from 'svelte-french-toast';
let loading: boolean = false; let loading: boolean = false;
let dialogOpen = false;
let form: SuperValidated<CreateServerSchemaType>; let form: SuperValidated<CreateServerSchemaType>;
</script> </script>
<Dialog> <Dialog bind:open={dialogOpen}>
<DialogTrigger asChild let:builder> <DialogTrigger asChild let:builder>
<slot {builder} /> <slot {builder} />
</DialogTrigger> </DialogTrigger>
@ -50,7 +47,7 @@
method={'POST'} method={'POST'}
let:config let:config
options={{ options={{
onSubmit: (s) => { onSubmit: () => {
loading = true; loading = true;
}, },
onError: (e) => { onError: (e) => {
@ -60,9 +57,11 @@
onResult: ({ result }) => { onResult: ({ result }) => {
loading = false; loading = false;
if (result.type === 'success') { if (result.type === 'success') {
goto(`/${result.data.serverId}`); dialogOpen = false;
toast.success('Server created successfully!');
} else { } else {
console.error('Server-failure: Result:', result); console.error('Server-failure: Result:', result);
toast.error('Server failed to create.');
} }
}, },
}} }}

View File

@ -10,13 +10,18 @@ export const load: PageServerLoad = async ({ params }) => {
const { serverId } = params; const { serverId } = params;
const exists = await WGServer.exists(serverId ?? ''); const exists = await WGServer.exists(serverId ?? '');
if (exists) { if (!exists) {
logger.warn(`Server not found. Redirecting to home page. ServerId: ${serverId}`);
throw redirect(303, '/');
}
const wg = new WGServer(serverId); const wg = new WGServer(serverId);
const server = await wg.get(); const server = await wg.get();
if (server.status === 'up') { if (server.status === 'up') {
const hasInterface = await wg.isUp(); const hasInterface = await wg.isUp();
if (!hasInterface) { if (!hasInterface) {
logger.debug(`Interface not found. Starting WireGuard. ServerId: ${serverId}`);
await wg.start(); await wg.start();
} }
} }
@ -27,9 +32,6 @@ export const load: PageServerLoad = async ({ params }) => {
server, server,
usage, usage,
}; };
}
throw error(404, 'Not found');
}; };
export const actions: Actions = { export const actions: Actions = {
@ -98,19 +100,19 @@ export const actions: Actions = {
const wg = new WGServer(server.id); const wg = new WGServer(server.id);
await wg.remove(); await wg.remove();
} }
return { ok: true };
} catch (e) { } catch (e) {
console.error('Exception:', e); logger.error(e);
throw error(500, 'Unhandled Exception'); throw error(500, 'Unhandled Exception');
} }
return redirect(303, '/');
}, },
'change-server-state': async ({ request, params }) => { 'change-server-state': async ({ request, params }) => {
const { serverId } = params; const { serverId } = params;
const server = await findServer(serverId ?? ''); const server = await findServer(serverId ?? '');
if (!server) { if (!server) {
logger.error('Action: ChangeState: Server not found'); logger.warn(`Action: ChangeState: Server not found. ServerId: ${serverId}`);
throw redirect(303, '/'); throw redirect(303, '/');
} }
@ -140,19 +142,20 @@ export const actions: Actions = {
break; break;
} }
} }
return { ok: true };
} catch (e) { } catch (e) {
logger.error({ logger.error({
message: 'Exception: ChangeState', message: `Exception: ChangeState. ServerId: ${serverId}`,
exception: e, exception: e,
}); });
throw error(500, 'Unhandled Exception'); throw error(500, 'Unhandled Exception');
} }
return { ok: true };
}, },
create: async (event) => { create: async (event) => {
const form = await superValidate(event, CreatePeerSchema); const form = await superValidate(event, CreatePeerSchema);
if (!form.valid) { if (!form.valid) {
logger.warn('CreatePeer: Bad Request: failed to validate form');
return setError(form, 'Bad Request'); return setError(form, 'Bad Request');
} }
@ -162,13 +165,13 @@ export const actions: Actions = {
try { try {
const server = await findServer(serverId ?? ''); const server = await findServer(serverId ?? '');
if (!server) { if (!server) {
console.error('Server not found'); logger.error(`Server not found. ServerId: ${serverId}`);
return setError(form, 'Server not found'); return setError(form, 'Server not found');
} }
const freeAddress = await WGServer.getFreePeerIp(server.id); const freeAddress = await WGServer.getFreePeerIp(server.id);
if (!freeAddress) { if (!freeAddress) {
console.error(`ERR: ServerId: ${serverId};`, 'No free addresses;'); logger.error(`No free addresses. ServerId: ${serverId}`);
return setError(form, 'No free addresses'); return setError(form, 'No free addresses');
} }
@ -186,13 +189,16 @@ export const actions: Actions = {
}); });
if (!addedPeer) { 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 setError(form, 'Failed to add peer');
} }
return { ok: true }; return { form };
} catch (e) { } catch (e) {
console.error('Exception:', e); logger.error({
message: `Exception: CreatePeer. ServerId: ${serverId}`,
exception: e,
});
return setError(form, 'Unhandled Exception'); return setError(form, 'Unhandled Exception');
} }
}, },

View File

@ -14,27 +14,27 @@
FormButton, FormButton,
FormField, FormField,
FormInput, FormInput,
FormItem,
FormLabel, FormLabel,
FormValidation, FormValidation,
} from '$lib/components/ui/form'; } from '$lib/components/ui/form';
import { FormItem } from '$lib/components/ui/form/index.js';
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { createEventDispatcher } from 'svelte'; import toast from 'svelte-french-toast';
const dispatch = createEventDispatcher();
let loading: boolean = false; let loading: boolean = false;
let dialogOpen = false;
let form: SuperValidated<CreatePeerSchemaType>; let form: SuperValidated<CreatePeerSchemaType>;
const handleSuccess = async () => { const handleSuccess = async () => {
await invalidateAll(); await invalidateAll();
dispatch('close', 'OK'); toast.success('Peer created!');
dialogOpen = false;
}; };
</script> </script>
<Dialog> <Dialog bind:open={dialogOpen}>
<DialogTrigger asChild let:builder> <DialogTrigger asChild let:builder>
<slot {builder} /> <slot {builder} />
</DialogTrigger> </DialogTrigger>
@ -50,7 +50,7 @@
method={'POST'} method={'POST'}
let:config let:config
options={{ options={{
onSubmit: (s) => { onSubmit: () => {
loading = true; loading = true;
}, },
onError: (e) => { onError: (e) => {
@ -61,6 +61,7 @@
if (result.type === 'success') { if (result.type === 'success') {
handleSuccess(); handleSuccess();
} else { } else {
toast.error('Failed to create peer');
console.error('Server-failure: Result:', result); console.error('Server-failure: Result:', result);
} }
loading = false; loading = false;

View File

@ -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 the host is not set, then we are using the server's public IP
if (!WG_HOST) { 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(); WG_HOST = resp.trim();
} }

18
web/tests/network.test.ts Normal file
View File

@ -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);
});
});

View File

@ -0,0 +1,8 @@
import { generateWgKey } from '$lib/wireguard';
describe('Keys', () => {
it('should generate a key', async () => {
const keys = await generateWgKey();
console.log(keys);
});
});