From ac5275168c24c2884baaa88562d5f66fab624dbb Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Thu, 21 Dec 2023 18:28:22 +0330 Subject: [PATCH] using `execa` for accessing cli tools --- web/src/lib/network.ts | 44 ++++++++-------------- web/src/lib/shell.ts | 42 --------------------- web/src/lib/wireguard/index.ts | 45 +++++++++++------------ web/src/routes/[serverId]/+page.server.ts | 7 +++- web/src/routes/api/health/+server.ts | 2 +- web/src/routes/api/host/+server.ts | 6 +-- 6 files changed, 46 insertions(+), 100 deletions(-) delete mode 100644 web/src/lib/shell.ts diff --git a/web/src/lib/network.ts b/web/src/lib/network.ts index 529bf43..85ed229 100644 --- a/web/src/lib/network.ts +++ b/web/src/lib/network.ts @@ -1,43 +1,31 @@ -import Shell from '$lib/shell'; +import { execaCommand } from 'execa'; +import logger from '$lib/logger'; export default class Network { - public static async createInterface(inet: string, address: string): Promise { - // First, check if the interface already exists. - const interfaces = await Shell.exec(`ip link show | grep ${inet}`, true); - if (interfaces.includes(`${inet}`)) { - console.error(`failed to create interface, ${inet} already exists!`); - return false; - } - - const o2 = await Shell.exec(`ip address add dev ${inet} ${address}`); - // check if it has any error - if (o2 !== '') { - console.error(`failed to assign ip to interface, ${o2}`); - console.log(`removing interface ${inet} due to errors`); - await Shell.exec(`ip link delete dev ${inet}`, true); - return false; - } - - return true; - } - public static async dropInterface(inet: string) { - await Shell.exec(`ip link delete dev ${inet}`, true); + await execaCommand(`ip link delete dev ${inet}`); } public static async defaultInterface(): Promise { - return await Shell.exec(`ip route list default | awk '{print $5}'`); + const { stdout: o } = await execaCommand(`ip route list default | awk '{print $5}'`); + return o.trim(); } - public static async checkInterfaceExists(inet: string): Promise { - return await Shell.exec(`ip link show | grep ${inet}`, true).then((o) => o.trim() !== ''); + public static async interfaceExists(inet: string): Promise { + try { + const { stdout: o } = await execaCommand(`ip link show | grep ${inet}`); + console.log(o); + return o.trim() !== ''; + } catch (e) { + logger.debug('Interface does not exist:', inet); + return false; + } } - public static async getInUsePorts(): Promise { + public static async inUsePorts(): Promise { const ports = []; - const output = await Shell.exec( + const { stdout: output } = await execaCommand( `netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`, - true, ); for (const line of output.split('\n')) { const clean = Number(line.trim()); diff --git a/web/src/lib/shell.ts b/web/src/lib/shell.ts deleted file mode 100644 index 28b53ce..0000000 --- a/web/src/lib/shell.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { exec } from 'child_process'; -import logger from '$lib/logger'; - -export default class Shell { - public static async exec( - command: string, - safe: boolean = false, - ...args: string[] - ): Promise { - if (process.platform !== 'linux') { - throw new Error('This program is not meant to run non UNIX systems'); - } - - return new Promise(async (resolve, reject) => { - const cmd = `${command}${args.length > 0 ? ` ${args.join(' ')}` : ''}`; - exec(cmd, { shell: 'bash' }, (err, stdout, stderr) => { - if (err) { - const message = `Command Failed: ${JSON.stringify( - { - cmd, - code: err.code, - killed: err.killed, - stderr, - }, - null, - 2, - )}`; - - if (safe) { - logger.debug(message); - return resolve(stderr); - } - - logger.error(message); - return reject(err); - } - - return resolve(String(stdout).trim()); - }); - }); - } -} diff --git a/web/src/lib/wireguard/index.ts b/web/src/lib/wireguard/index.ts index 09deb3b..8a7d3e6 100644 --- a/web/src/lib/wireguard/index.ts +++ b/web/src/lib/wireguard/index.ts @@ -3,7 +3,6 @@ import path from 'path'; import deepmerge from 'deepmerge'; import type { Peer, WgKey, WgServer } from '$lib/typings'; import Network from '$lib/network'; -import Shell from '$lib/shell'; import { WG_PATH, WG_SEVER_PATH } from '$lib/constants'; import { dynaJoin, isJson } from '$lib/utils'; import { getPeerConf } from '$lib/wireguard/utils'; @@ -51,8 +50,8 @@ export class WGServer { async stop(): Promise { const server = await this.get(); - if (await Network.checkInterfaceExists(`wg${server.confId}`)) { - await Shell.exec(`wg-quick down wg${server.confId}`, true); + if (await Network.interfaceExists(`wg${server.confId}`)) { + await execaCommand(`wg-quick down wg${server.confId}`); } await this.update({ status: 'down' }); @@ -67,11 +66,14 @@ export class WGServer { await this.writeConfigFile(server); } - if (await Network.checkInterfaceExists(`wg${server.confId}`)) { - await Shell.exec(`wg-quick down wg${server.confId}`, true); + const isAlreadyUp = await this.isUp(); + 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 Shell.exec(`wg-quick up wg${server.confId}`); + await execaCommand(`wg-quick up wg${server.confId}`); await this.update({ status: 'up' }); return true; @@ -132,7 +134,7 @@ export class WGServer { await this.update({ confHash: getConfigHash(wg.confId) }); } - async hasInterface(): Promise { + async isUp(): Promise { const server = await this.get(); try { const res = await execaCommand(`wg show wg${server.confId}`); @@ -144,7 +146,7 @@ export class WGServer { async getUsage(): Promise { const server = await this.get(); - const hasInterface = await this.hasInterface(); + const hasInterface = await this.isUp(); const usages: WgUsage = { total: { rx: 0, tx: 0 }, @@ -371,14 +373,6 @@ function resolveConfigPath(confId: number): string { return path.resolve(path.join(WG_PATH, `wg${confId}.conf`)); } -/** - * This function is for checking out WireGuard server is running - */ -async function wgCheckout(configId: number): Promise { - const res = await Shell.exec(`ip link show | grep wg${configId}`, true); - return res.includes(`wg${configId}`); -} - export async function readWgConf(configId: number): Promise { const confPath = resolveConfigPath(configId); const conf = fs.readFileSync(confPath, 'utf-8'); @@ -462,7 +456,10 @@ export async function readWgConf(configId: number): Promise { reachedPeers = true; } } - server.status = (await wgCheckout(configId)) ? 'up' : 'down'; + + const hasInterface = await Network.interfaceExists(`wg${configId}`); + server.status = hasInterface ? 'up' : 'down'; + return server; } @@ -505,9 +502,9 @@ function wgPeersStr(configId: number): string[] { } export async function generateWgKey(): Promise { - const privateKey = await Shell.exec('wg genkey'); - const publicKey = await Shell.exec(`echo ${privateKey} | wg pubkey`); - const preSharedKey = await Shell.exec('wg genkey'); + const { stdout: privateKey } = await execaCommand('wg genkey'); + const { stdout: publicKey } = await execaCommand(`echo ${privateKey} | wg pubkey`); + const { stdout: preSharedKey } = await execaCommand('wg genkey'); return { privateKey, publicKey, preSharedKey }; } @@ -579,10 +576,10 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise< await wg.update({ confHash: getConfigHash(confId) }); // to ensure interface does not exists - await Shell.exec(`wg-quick down wg${confId}`, true); + await wg.stop(); // restart WireGuard - await Shell.exec(`wg-quick up wg${confId}`); + await wg.start(); // return server id return uuid; @@ -595,7 +592,7 @@ export async function isIPReserved(ip: string): Promise { export async function isPortReserved(port: number): Promise { const inUsePorts = [ - await Network.getInUsePorts(), + await Network.inUsePorts(), (await getServers()).map((s) => Number(s.listen)), ].flat(); return inUsePorts.includes(port); @@ -676,7 +673,7 @@ export async function findServer( export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: string }> { const inet = await Network.defaultInterface(); - const inet_address = await Shell.exec(`hostname -i | awk '{print $1}'`); + const { stdout: inet_address } = await execaCommand(`hostname -i | awk '{print $1}'`); const source = `${s.address}/24`; const wg_inet = `wg${s.confId}`; diff --git a/web/src/routes/[serverId]/+page.server.ts b/web/src/routes/[serverId]/+page.server.ts index 607376b..bfba3d9 100644 --- a/web/src/routes/[serverId]/+page.server.ts +++ b/web/src/routes/[serverId]/+page.server.ts @@ -15,7 +15,7 @@ export const load: PageServerLoad = async ({ params }) => { const server = await wg.get(); if (server.status === 'up') { - const hasInterface = await wg.hasInterface(); + const hasInterface = await wg.isUp(); if (!hasInterface) { await wg.start(); } @@ -143,7 +143,10 @@ export const actions: Actions = { return { ok: true }; } catch (e) { - logger.error('Exception: ChangeState:', e); + logger.error({ + message: 'Exception: ChangeState', + exception: e, + }); throw error(500, 'Unhandled Exception'); } }, diff --git a/web/src/routes/api/health/+server.ts b/web/src/routes/api/health/+server.ts index 6be0a2e..472c90b 100644 --- a/web/src/routes/api/health/+server.ts +++ b/web/src/routes/api/health/+server.ts @@ -7,7 +7,7 @@ export const GET: RequestHandler = async () => { for (const { id } of await getServers()) { const wg = new WGServer(id); const server = await wg.get(); - const hasInterface = await wg.hasInterface(); + const hasInterface = await wg.isUp(); // If the server is up and the interface doesn't exist, start it if (server.status === 'up' && !hasInterface) { diff --git a/web/src/routes/api/host/+server.ts b/web/src/routes/api/host/+server.ts index 7fede8b..05805f4 100644 --- a/web/src/routes/api/host/+server.ts +++ b/web/src/routes/api/host/+server.ts @@ -1,14 +1,14 @@ import type { RequestHandler } from '@sveltejs/kit'; -import Shell from '$lib/shell'; -import 'dotenv/config'; import logger from '$lib/logger'; +import { execaCommand } from 'execa'; +import 'dotenv/config'; export const GET: RequestHandler = async () => { let { WG_HOST } = process.env; // if the host is not set, then we are using the server's public IP if (!WG_HOST) { - const resp = await Shell.exec('curl -s ifconfig.me', true); + const { stdout: resp } = await execaCommand('curl -s ifconfig.me'); WG_HOST = resp.trim(); }