Harden admin API routes and shell usage
- Require admin auth on interface-local-ip, network-interfaces, next-listen-port and public-ip (interface-local-ip and public-ip allowed unauthenticated command injection as root) - Validate interface names and call ip/curl via execFileSync - Write host/client WireGuard configs with fs instead of shell heredocs, validating IPs, AllowedIPs, endpoint and listen port - Replace rm -rf/cat/mkdir/chmod/test shell calls with fs APIs - Refuse private media requested through the public media route Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
|
import checkUserAccess from "./check-user-access";
|
||||||
|
import userAuth from "./user-auth";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
req: Request;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for admin API routes. Returns an error
|
||||||
|
* response if the request is not from a logged in
|
||||||
|
* user with admin access, otherwise `undefined`
|
||||||
|
*/
|
||||||
|
export default async function adminApiGuard({
|
||||||
|
req,
|
||||||
|
}: Params): Promise<APIResponseObject | undefined> {
|
||||||
|
const { user, user_types } = await userAuth({ req });
|
||||||
|
|
||||||
|
if (!user?.logged_in_status) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
logoutUser: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import grabHostDirnames from "./grab-host-dir-names";
|
|||||||
import grabClientDirnames from "./grab-client-dir-names";
|
import grabClientDirnames from "./grab-client-dir-names";
|
||||||
import removeSubnetFirewallRules from "../firewall/remove-subnet-firewall-rules";
|
import removeSubnetFirewallRules from "../firewall/remove-subnet-firewall-rules";
|
||||||
import { AppData } from "@/src/data/app-data";
|
import { AppData } from "@/src/data/app-data";
|
||||||
import { execSync } from "node:child_process";
|
import { rmSync } from "node:fs";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
host_id: number;
|
host_id: number;
|
||||||
@@ -56,7 +56,7 @@ export default async function deleteWireguardHost({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`rm -rf ${CLIENT_DIR}`, { encoding: "utf-8" });
|
rmSync(CLIENT_DIR, { recursive: true, force: true });
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
await BunSQLite.delete<BUN_SQLITE_WGUI_CLIENTS, TableType>({
|
await BunSQLite.delete<BUN_SQLITE_WGUI_CLIENTS, TableType>({
|
||||||
@@ -112,7 +112,7 @@ export default async function deleteWireguardHost({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`rm -rf ${HOST_CONFIG_DIR}`, { encoding: "utf-8" });
|
rmSync(HOST_CONFIG_DIR, { recursive: true, force: true });
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const del_host = await BunSQLite.sql({
|
const del_host = await BunSQLite.sql({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { execSync } from "node:child_process";
|
import validateInterfaceName from "@/src/utils/validate-interface-name";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the local IPv4 address of a specific network interface
|
* Get the local IPv4 address of a specific network interface
|
||||||
@@ -8,10 +9,11 @@ import { execSync } from "node:child_process";
|
|||||||
export default async function getInterfaceLocalIP(
|
export default async function getInterfaceLocalIP(
|
||||||
interface_name: string | null,
|
interface_name: string | null,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!interface_name) return ``;
|
if (!validateInterfaceName({ name: interface_name })) return ``;
|
||||||
try {
|
try {
|
||||||
const output = execSync(
|
const output = execFileSync(
|
||||||
`ip -4 addr show ${interface_name}`,
|
"ip",
|
||||||
|
["-4", "addr", "show", interface_name as string],
|
||||||
{ encoding: "utf-8" },
|
{ encoding: "utf-8" },
|
||||||
).trim();
|
).trim();
|
||||||
const match = output.match(
|
const match = output.match(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
const VIRTUAL_INTERFACE_PATTERNS = [
|
const VIRTUAL_INTERFACE_PATTERNS = [
|
||||||
@@ -25,15 +26,7 @@ function isVirtualInterface(name: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPhysicalInterface(name: string): boolean {
|
function isPhysicalInterface(name: string): boolean {
|
||||||
try {
|
return existsSync(`/sys/class/net/${name}/device`);
|
||||||
const devicePath = `/sys/class/net/${name}/device`;
|
|
||||||
const result = execSync(`test -e ${devicePath} && echo yes || echo no`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
return result === "yes";
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { execSync } from "node:child_process";
|
import validateInterfaceName from "@/src/utils/validate-interface-name";
|
||||||
|
import { execFileSync, execSync } from "node:child_process";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to grab the host's public
|
* Function to grab the host's public
|
||||||
@@ -9,12 +10,20 @@ export default async function grabHostPublicIPAddress(
|
|||||||
interface_name?: string | null,
|
interface_name?: string | null,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const curl_cmd = interface_name
|
const interface_args = validateInterfaceName({ name: interface_name })
|
||||||
? `curl -sS --max-time 15 --interface ${interface_name} https://api.ipify.org`
|
? ["--interface", interface_name as string]
|
||||||
: `curl -sS --max-time 15 https://api.ipify.org`;
|
: [];
|
||||||
const public_ip = execSync(curl_cmd, {
|
const public_ip = execFileSync(
|
||||||
encoding: "utf-8",
|
"curl",
|
||||||
})
|
[
|
||||||
|
"-sS",
|
||||||
|
"--max-time",
|
||||||
|
"15",
|
||||||
|
...interface_args,
|
||||||
|
"https://api.ipify.org",
|
||||||
|
],
|
||||||
|
{ encoding: "utf-8" },
|
||||||
|
)
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(public_ip)) {
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(public_ip)) {
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import type {
|
|||||||
import { AppData } from "@/src/data/app-data";
|
import { AppData } from "@/src/data/app-data";
|
||||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import validateIpv4 from "@/src/utils/validate-ipv4";
|
||||||
|
import validateAllowedIps from "@/src/utils/validate-allowed-ips";
|
||||||
|
import validateEndpointHost from "@/src/utils/validate-endpoint-host";
|
||||||
import grabHostPublicIPAddress from "./grab-host-public-ip-address";
|
import grabHostPublicIPAddress from "./grab-host-public-ip-address";
|
||||||
import setupWireguardHost from "./setup-wireguard-host";
|
import setupWireguardHost from "./setup-wireguard-host";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
@@ -72,6 +76,24 @@ export default async function setupWireguardClient({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateIpv4({ ip: CLIENT_WG_IP })) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Invalid Client Private IP address: ${CLIENT_WG_IP}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
client?.allow_all_ips != 1 &&
|
||||||
|
client?.allowed_ips &&
|
||||||
|
!validateAllowedIps({ value: client.allowed_ips })
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Invalid Allowed IPs: ${client.allowed_ips}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const HOST_WG_IP = resolved_host?.wg_ip_address;
|
const HOST_WG_IP = resolved_host?.wg_ip_address;
|
||||||
|
|
||||||
if (!HOST_WG_IP) {
|
if (!HOST_WG_IP) {
|
||||||
@@ -104,53 +126,57 @@ export default async function setupWireguardClient({
|
|||||||
client?.public_ip_address ||
|
client?.public_ip_address ||
|
||||||
HOST_WG_IP;
|
HOST_WG_IP;
|
||||||
|
|
||||||
const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
if (!validateEndpointHost({ value: PUBLIC_IP_ADDRESS })) {
|
||||||
encoding: "utf-8",
|
return {
|
||||||
}).trim();
|
success: false,
|
||||||
|
msg: `Invalid Host public IP address: ${PUBLIC_IP_ADDRESS}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
const LISTEN_PORT = Number(
|
||||||
encoding: "utf-8",
|
resolved_host?.listen_port || AppData["DefaultWGListenPort"],
|
||||||
}).trim();
|
);
|
||||||
|
|
||||||
const HOST_PUBLIC_KEY = execSync(
|
if (!Number.isInteger(LISTEN_PORT)) {
|
||||||
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
return {
|
||||||
{ encoding: "utf-8" },
|
success: false,
|
||||||
|
msg: `Invalid Host listen port: ${resolved_host?.listen_port}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLIENT_PRIVATE_KEY = readFileSync(
|
||||||
|
CLIENT_PRIVATE_KEY_FILE,
|
||||||
|
"utf-8",
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
let sh = ``;
|
const HOST_PUBLIC_KEY = readFileSync(
|
||||||
|
path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME),
|
||||||
|
"utf-8",
|
||||||
|
).trim();
|
||||||
|
|
||||||
sh += `cd ${CLIENT_DIR}\n`;
|
let CLIENT_CONFIG = ``;
|
||||||
sh += `cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF\n`;
|
|
||||||
sh += `\n`;
|
CLIENT_CONFIG += `\n`;
|
||||||
sh += `[Interface]\n`;
|
CLIENT_CONFIG += `[Interface]\n`;
|
||||||
sh += `Address = ${CLIENT_WG_IP}/32\n`;
|
CLIENT_CONFIG += `Address = ${CLIENT_WG_IP}/32\n`;
|
||||||
sh += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`;
|
CLIENT_CONFIG += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`;
|
||||||
if (client?.allow_all_ips == 1) {
|
if (client?.allow_all_ips == 1) {
|
||||||
sh += `DNS = 1.1.1.1\n`;
|
CLIENT_CONFIG += `DNS = 1.1.1.1\n`;
|
||||||
}
|
}
|
||||||
sh += `\n`;
|
CLIENT_CONFIG += `\n`;
|
||||||
sh += `[Peer]\n`;
|
CLIENT_CONFIG += `[Peer]\n`;
|
||||||
sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
|
CLIENT_CONFIG += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
|
||||||
sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:${resolved_host?.listen_port || AppData["DefaultWGListenPort"]}\n`;
|
CLIENT_CONFIG += `Endpoint = ${PUBLIC_IP_ADDRESS}:${LISTEN_PORT}\n`;
|
||||||
|
|
||||||
if (client?.allow_all_ips == 1) {
|
if (client?.allow_all_ips == 1) {
|
||||||
sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
|
CLIENT_CONFIG += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
|
||||||
} else if (client?.allowed_ips) {
|
} else if (client?.allowed_ips) {
|
||||||
sh += `AllowedIPs = ${client.allowed_ips}\n`;
|
CLIENT_CONFIG += `AllowedIPs = ${client.allowed_ips}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
sh += `PersistentKeepalive = 25\n`;
|
CLIENT_CONFIG += `PersistentKeepalive = 25\n`;
|
||||||
|
|
||||||
sh += `EOF\n`;
|
writeFileSync(CLIENT_CONFIG_FILE, CLIENT_CONFIG, { mode: 0o600 });
|
||||||
sh += `\n`;
|
|
||||||
|
|
||||||
const exec = execSync(sh, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
const CLIENT_CONFIG = execSync(`cat ${CLIENT_CONFIG_FILE}`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
|
|
||||||
const host_setup_res = skip_host_setup
|
const host_setup_res = skip_host_setup
|
||||||
? { success: true }
|
? { success: true }
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type {
|
|||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import validateIpv4 from "@/src/utils/validate-ipv4";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type { TableType, User } from "@/src/types";
|
import type { TableType, User } from "@/src/types";
|
||||||
@@ -125,6 +127,24 @@ export default async function setupWireguardHost({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateIpv4({ ip: HOST_WG_IP })) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Invalid Host Private IP address: ${HOST_WG_IP}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const LISTEN_PORT = Number(
|
||||||
|
host?.listen_port || AppData["DefaultWGListenPort"],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Number.isInteger(LISTEN_PORT)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Invalid Host listen port: ${host?.listen_port}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const is_ip_available = is_update_after_client_setup
|
const is_ip_available = is_update_after_client_setup
|
||||||
? { success: true }
|
? { success: true }
|
||||||
: host?.wg_ip_address
|
: host?.wg_ip_address
|
||||||
@@ -163,13 +183,12 @@ export default async function setupWireguardHost({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const HOST_PUBLIC_KEY = execSync(`cat ${HOST_PUBLIC_KEY_FILE}`, {
|
const HOST_PUBLIC_KEY = readFileSync(HOST_PUBLIC_KEY_FILE, "utf-8").trim();
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
const HOST_PRIVATE_KEY = execSync(`cat ${HOST_PRIVATE_KEY_FILE}`, {
|
const HOST_PRIVATE_KEY = readFileSync(
|
||||||
encoding: "utf-8",
|
HOST_PRIVATE_KEY_FILE,
|
||||||
}).trim();
|
"utf-8",
|
||||||
|
).trim();
|
||||||
|
|
||||||
if (!is_update_after_client_setup) {
|
if (!is_update_after_client_setup) {
|
||||||
const update_host = await BunSQLite.insert<
|
const update_host = await BunSQLite.insert<
|
||||||
@@ -189,12 +208,6 @@ export default async function setupWireguardHost({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sh = ``;
|
|
||||||
|
|
||||||
sh += `set -e\n`;
|
|
||||||
|
|
||||||
sh += `cd ${HOST_CONFIG_DIR}\n`;
|
|
||||||
|
|
||||||
const HOST_SUBNET = `${HOST_WG_IP.split(".").slice(0, 3).join(".")}.0/24`;
|
const HOST_SUBNET = `${HOST_WG_IP.split(".").slice(0, 3).join(".")}.0/24`;
|
||||||
|
|
||||||
const iptables_scripts = buildHostIptablesScripts({
|
const iptables_scripts = buildHostIptablesScripts({
|
||||||
@@ -224,53 +237,53 @@ export default async function setupWireguardHost({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
execSync(`mkdir -p ${HOST_IPTABLES_DIR}`, { encoding: "utf-8" });
|
mkdirSync(HOST_IPTABLES_DIR, { recursive: true });
|
||||||
await Bun.write(POST_UP_PATH, iptables_scripts.post_up);
|
await Bun.write(POST_UP_PATH, iptables_scripts.post_up);
|
||||||
await Bun.write(POST_DOWN_PATH, iptables_scripts.post_down);
|
await Bun.write(POST_DOWN_PATH, iptables_scripts.post_down);
|
||||||
execSync(`chmod +x ${POST_UP_PATH} ${POST_DOWN_PATH}`, {
|
chmodSync(POST_UP_PATH, 0o755);
|
||||||
encoding: "utf-8",
|
chmodSync(POST_DOWN_PATH, 0o755);
|
||||||
});
|
|
||||||
|
|
||||||
sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`;
|
let HOST_CONFIG = ``;
|
||||||
sh += `[Interface]\n`;
|
|
||||||
sh += `Address = ${HOST_WG_IP}/24\n`;
|
HOST_CONFIG += `[Interface]\n`;
|
||||||
sh += `ListenPort = ${host?.listen_port || AppData["DefaultWGListenPort"]}\n`;
|
HOST_CONFIG += `Address = ${HOST_WG_IP}/24\n`;
|
||||||
sh += `PrivateKey = ${HOST_PRIVATE_KEY}\n`;
|
HOST_CONFIG += `ListenPort = ${LISTEN_PORT}\n`;
|
||||||
sh += `PostUp = ${POST_UP_PATH}\n`;
|
HOST_CONFIG += `PrivateKey = ${HOST_PRIVATE_KEY}\n`;
|
||||||
sh += `PostDown = ${POST_DOWN_PATH}\n`;
|
HOST_CONFIG += `PostUp = ${POST_UP_PATH}\n`;
|
||||||
sh += `\n`;
|
HOST_CONFIG += `PostDown = ${POST_DOWN_PATH}\n`;
|
||||||
|
HOST_CONFIG += `\n`;
|
||||||
|
|
||||||
if (clients[0]) {
|
if (clients[0]) {
|
||||||
for (let i = 0; i < clients.length; i++) {
|
for (let i = 0; i < clients.length; i++) {
|
||||||
const client = clients[i];
|
const client = clients[i];
|
||||||
if (!client?.id) continue;
|
if (!client?.id) continue;
|
||||||
|
|
||||||
|
if (!validateIpv4({ ip: client.wg_ip_address })) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Invalid Private IP address for client ${client.id}: ${client.wg_ip_address}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { CLIENT_PUBLIC_KEY_FILE } = grabClientDirnames({
|
const { CLIENT_PUBLIC_KEY_FILE } = grabClientDirnames({
|
||||||
client,
|
client,
|
||||||
host_id: HOST_ID,
|
host_id: HOST_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
const CLIENT_PUBLIC_KEY = execSync(
|
const CLIENT_PUBLIC_KEY = readFileSync(
|
||||||
`cat ${CLIENT_PUBLIC_KEY_FILE}`,
|
CLIENT_PUBLIC_KEY_FILE,
|
||||||
{
|
"utf-8",
|
||||||
encoding: "utf-8",
|
|
||||||
},
|
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
sh += `[Peer]\n`;
|
HOST_CONFIG += `[Peer]\n`;
|
||||||
sh += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`;
|
HOST_CONFIG += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`;
|
||||||
sh += `AllowedIPs = ${client.wg_ip_address}/32\n`;
|
HOST_CONFIG += `AllowedIPs = ${client.wg_ip_address}/32\n`;
|
||||||
sh += `\n`;
|
HOST_CONFIG += `\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sh += `EOF\n`;
|
writeFileSync(HOST_CONFIG_FILE, HOST_CONFIG, { mode: 0o600 });
|
||||||
|
chmodSync(HOST_CONFIG_FILE, 0o600);
|
||||||
sh += `chmod 600 ${INTERFACE_NAME}.conf\n`;
|
|
||||||
|
|
||||||
sh += `\n`;
|
|
||||||
|
|
||||||
const exec = execSync(sh, { encoding: "utf-8" });
|
|
||||||
|
|
||||||
if (!is_update_after_client_setup) {
|
if (!is_update_after_client_setup) {
|
||||||
execSync(
|
execSync(
|
||||||
@@ -293,7 +306,7 @@ export default async function setupWireguardHost({
|
|||||||
|
|
||||||
const firewall_res = await allowSubnetFirewallRules({
|
const firewall_res = await allowSubnetFirewallRules({
|
||||||
subnet: HOST_SUBNET,
|
subnet: HOST_SUBNET,
|
||||||
listen_port: host?.listen_port || AppData["DefaultWGListenPort"],
|
listen_port: LISTEN_PORT,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!firewall_res.success) {
|
if (!firewall_res.success) {
|
||||||
@@ -304,7 +317,7 @@ export default async function setupWireguardHost({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
msg: [exec.trim(), manage_res.msg, firewall_res.msg].join("\n\n"),
|
msg: [manage_res.msg, firewall_res.msg].join("\n\n"),
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ export default async function handleMediaServer({
|
|||||||
throw new Error(`Media record not found!`);
|
throw new Error(`Media record not found!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (privacy != "private" && media.is_private == 1) {
|
||||||
|
throw new Error(`Media record not found!`);
|
||||||
|
}
|
||||||
|
|
||||||
const media_location = thumbnail
|
const media_location = thumbnail
|
||||||
? media.media_thumbnail_write_path
|
? media.media_thumbnail_write_path
|
||||||
: media.media_write_path;
|
: media.media_write_path;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { execSync } from "node:child_process";
|
import { rmSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { AdminCrudAPIParams, TableType } from "@/src/types";
|
import type { AdminCrudAPIParams, TableType } from "@/src/types";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
BUN_SQLITE_WGUI_HOSTS,
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types";
|
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/types";
|
||||||
import grabClientDirnames from "@/src/functions/backend/setup/grab-client-dir-names";
|
import grabClientDirnames from "@/src/functions/backend/setup/grab-client-dir-names";
|
||||||
|
|
||||||
const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_LIB_HOST_CLIENTS_DIR_NAME } =
|
const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_LIB_HOST_CLIENTS_DIR_NAME } =
|
||||||
@@ -72,7 +72,7 @@ export default async function (
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`rm -rf ${CLIENT_DIR}`, { encoding: "utf-8" });
|
rmSync(CLIENT_DIR, { recursive: true, force: true });
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
affected_host_ids.add(host_id);
|
affected_host_ids.add(host_id);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import getInterfaceLocalIP from "@/src/functions/backend/setup/get-interface-local-ip";
|
import getInterfaceLocalIP from "@/src/functions/backend/setup/get-interface-local-ip";
|
||||||
import type { ApiReqParams } from "@/src/types";
|
import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard";
|
||||||
import type {
|
import type {
|
||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
BunextAPIRouteHandler,
|
BunextAPIRouteHandler,
|
||||||
@@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
if (params.req.method !== "GET") {
|
if (params.req.method !== "GET") {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unauthorized_res = await adminApiGuard({ req: params.req });
|
||||||
|
|
||||||
|
if (unauthorized_res) {
|
||||||
|
return unauthorized_res;
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(params.req.url || "");
|
const url = new URL(params.req.url || "");
|
||||||
const iface = url.searchParams.get("interface") || undefined;
|
const iface = url.searchParams.get("interface") || undefined;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
|
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
|
||||||
import type { ApiReqParams } from "@/src/types";
|
import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard";
|
||||||
import type {
|
import type {
|
||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
BunextAPIRouteHandler,
|
BunextAPIRouteHandler,
|
||||||
@@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
if (params.req.method !== "GET") {
|
if (params.req.method !== "GET") {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unauthorized_res = await adminApiGuard({ req: params.req });
|
||||||
|
|
||||||
|
if (unauthorized_res) {
|
||||||
|
return unauthorized_res;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const interfaces = await grabHostNetworkInterfaces();
|
const interfaces = await grabHostNetworkInterfaces();
|
||||||
return { success: true, stringRes: JSON.stringify(interfaces) };
|
return { success: true, stringRes: JSON.stringify(interfaces) };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port";
|
import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port";
|
||||||
import type { ApiReqParams } from "@/src/types";
|
import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard";
|
||||||
import type {
|
import type {
|
||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
BunextAPIRouteHandler,
|
BunextAPIRouteHandler,
|
||||||
@@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
if (params.req.method !== "GET") {
|
if (params.req.method !== "GET") {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unauthorized_res = await adminApiGuard({ req: params.req });
|
||||||
|
|
||||||
|
if (unauthorized_res) {
|
||||||
|
return unauthorized_res;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const port = await getNextAvailableListenPort();
|
const port = await getNextAvailableListenPort();
|
||||||
return { success: true, numberRes: port };
|
return { success: true, numberRes: port };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import grabHostPublicIPAddress from "@/src/functions/backend/setup/grab-host-public-ip-address";
|
import grabHostPublicIPAddress from "@/src/functions/backend/setup/grab-host-public-ip-address";
|
||||||
import type { ApiReqParams } from "@/src/types";
|
import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard";
|
||||||
import type {
|
import type {
|
||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
BunextAPIRouteHandler,
|
BunextAPIRouteHandler,
|
||||||
@@ -11,6 +11,13 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
if (params.req.method !== "GET") {
|
if (params.req.method !== "GET") {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unauthorized_res = await adminApiGuard({ req: params.req });
|
||||||
|
|
||||||
|
if (unauthorized_res) {
|
||||||
|
return unauthorized_res;
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(params.req.url || "");
|
const url = new URL(params.req.url || "");
|
||||||
const iface = url.searchParams.get("interface") || undefined;
|
const iface = url.searchParams.get("interface") || undefined;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import validateAllowedIps from "./validate-allowed-ips";
|
||||||
|
|
||||||
|
describe("validateAllowedIps", () => {
|
||||||
|
test("accepts single and comma-separated CIDRs", () => {
|
||||||
|
expect(validateAllowedIps({ value: "10.0.0.0/24" })).toBe(true);
|
||||||
|
expect(validateAllowedIps({ value: "10.0.0.0/24, 192.168.1.5/32" })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects empty and unsafe values", () => {
|
||||||
|
expect(validateAllowedIps({ value: "" })).toBe(false);
|
||||||
|
expect(validateAllowedIps({ value: "$(id)" })).toBe(false);
|
||||||
|
expect(validateAllowedIps({ value: "10.0.0.0/24\nPostUp = id" })).toBe(false);
|
||||||
|
expect(validateAllowedIps({ value: "10.0.0.0/24," })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import validateIpv4Cidr from "./validate-ipv4-cidr";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
value?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a comma-separated list of IPv4
|
||||||
|
* addresses/CIDRs (WireGuard `AllowedIPs`)
|
||||||
|
*/
|
||||||
|
export default function validateAllowedIps({ value }: Params) {
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.every((entry) => validateIpv4Cidr({ value: entry.trim() }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import validateEndpointHost from "./validate-endpoint-host";
|
||||||
|
|
||||||
|
describe("validateEndpointHost", () => {
|
||||||
|
test("accepts IPv4 addresses and hostnames", () => {
|
||||||
|
expect(validateEndpointHost({ value: "203.0.113.4" })).toBe(true);
|
||||||
|
expect(validateEndpointHost({ value: "vpn.example.com" })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects empty and unsafe values", () => {
|
||||||
|
expect(validateEndpointHost({ value: "" })).toBe(false);
|
||||||
|
expect(validateEndpointHost({ value: "1.2.3.4\nPostUp = id" })).toBe(false);
|
||||||
|
expect(validateEndpointHost({ value: "$(id)" })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import validateIpv4 from "./validate-ipv4";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
value?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a WireGuard `Endpoint` host: an IPv4
|
||||||
|
* address or a DNS hostname
|
||||||
|
*/
|
||||||
|
export default function validateEndpointHost({ value }: Params) {
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateIpv4({ ip: value })) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import validateInterfaceName from "./validate-interface-name";
|
||||||
|
|
||||||
|
describe("validateInterfaceName", () => {
|
||||||
|
test("accepts common interface names", () => {
|
||||||
|
expect(validateInterfaceName({ name: "eth0" })).toBe(true);
|
||||||
|
expect(validateInterfaceName({ name: "enp2s0" })).toBe(true);
|
||||||
|
expect(validateInterfaceName({ name: "wgui1" })).toBe(true);
|
||||||
|
expect(validateInterfaceName({ name: "eth0.100" })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects empty, long, and shell-unsafe names", () => {
|
||||||
|
expect(validateInterfaceName({ name: "" })).toBe(false);
|
||||||
|
expect(validateInterfaceName({ name: null })).toBe(false);
|
||||||
|
expect(validateInterfaceName({ name: "a".repeat(16) })).toBe(false);
|
||||||
|
expect(validateInterfaceName({ name: "lo;id" })).toBe(false);
|
||||||
|
expect(validateInterfaceName({ name: "lo $(id)" })).toBe(false);
|
||||||
|
expect(validateInterfaceName({ name: "../x" })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
type Params = {
|
||||||
|
name?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a Linux network interface name
|
||||||
|
* (max 15 chars, no whitespace or shell characters)
|
||||||
|
*/
|
||||||
|
export default function validateInterfaceName({ name }: Params) {
|
||||||
|
if (!name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /^[A-Za-z0-9_.:-]{1,15}$/.test(name);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user