Browse Source

update backend

pull/1244/head
Bernd Storath 8 months ago
parent
commit
c745f84d7f
  1. 14
      src/i18n.config.ts
  2. 24
      src/server/api/cnf/:clientsOnteTimeLink.ts
  3. 4
      src/server/api/remember-me.get.ts
  4. 19
      src/server/api/session.post.ts
  5. 5
      src/server/api/ui-sort-clients.get.ts
  6. 5
      src/server/api/wg-enable-expire-time.get.ts
  7. 5
      src/server/api/wg-enable-one-time-links.get.ts
  8. 12
      src/server/api/wireguard/client/[clientId]/expireDate.put.ts
  9. 14
      src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts
  10. 7
      src/server/api/wireguard/client/index.post.ts
  11. 2
      src/server/middleware/session.ts
  12. 195
      src/server/utils/WireGuard.ts
  13. 15
      src/server/utils/config.ts
  14. 10
      src/server/utils/password.ts
  15. 25
      src/server/utils/types.ts

14
src/i18n.config.ts

@ -373,8 +373,15 @@ export default defineI18nConfig(() => ({
downloadConfig: '구성 다운로드',
madeBy: '만든 사람',
donate: '기부',
toggleCharts: '차트 표시/숨기기',
theme: { dark: '어두운 테마', light: '밝은 테마', auto: '자동 테마' },
restore: '복원',
backup: '백업',
titleRestoreConfig: '구성 파일 복원',
titleBackupConfig: '구성 파일 백업',
},
vi: {
// https://github.com/hoangneeee
name: 'Tên',
password: 'Mật khẩu',
signIn: 'Đăng nhập',
@ -400,6 +407,13 @@ export default defineI18nConfig(() => ({
downloadConfig: 'Tải xuống cấu hình',
madeBy: 'Được tạo bởi',
donate: 'Ủng hộ',
toggleCharts: 'Mở/Ẩn Biểu đồ',
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
restore: 'Khôi phục',
backup: 'Sao lưu',
titleRestoreConfig: 'Khôi phục cấu hình của bạn',
titleBackupConfig: 'Sao lưu cấu hình của bạn',
sort: 'Sắp xếp',
},
nl: {
name: 'Naam',

24
src/server/api/cnf/:clientsOnteTimeLink.ts

@ -0,0 +1,24 @@
export default defineEventHandler(async (event) => {
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
throw createError({
status: 404,
message: 'Invalid state',
});
}
const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink');
const clients = await WireGuard.getClients();
const client = clients.find(
(client) => client.oneTimeLink === clientOneTimeLink
);
if (!client) return;
const clientId = client.id;
const config = await WireGuard.getClientConfiguration({ clientId });
await WireGuard.eraseOneTimeLink({ clientId });
setHeader(
event,
'Content-Disposition',
`attachment; filename="${clientOneTimeLink}.conf"`
);
setHeader(event, 'Content-Type', 'text/plain');
return config;
});

4
src/server/api/remember-me.get.ts

@ -0,0 +1,4 @@
export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json');
return MAX_AGE > 0;
});

19
src/server/api/session.post.ts

@ -1,6 +1,7 @@
import type { SessionConfig } from 'h3';
export default defineEventHandler(async (event) => {
const session = await useWGSession(event);
const { password } = await readValidatedBody(
const { password, remember } = await readValidatedBody(
event,
validateZod(passwordType)
);
@ -13,13 +14,25 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Invalid state',
});
}
if (!isPasswordValid(password)) {
if (!isPasswordValid(password, PASSWORD_HASH)) {
throw createError({
statusCode: 401,
statusMessage: 'Incorrect Password',
});
}
const conf: SessionConfig = SESSION_CONFIG;
if (MAX_AGE && remember) {
conf.cookie = {
...(SESSION_CONFIG.cookie ?? {}),
maxAge: MAX_AGE,
};
}
const session = await useSession(event, {
...SESSION_CONFIG,
});
const data = await session.update({
authenticated: true,
});

5
src/server/api/ui-sort-clients.get.ts

@ -0,0 +1,5 @@
export default defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
const sort = UI_ENABLE_SORT_CLIENTS;
return sort === 'true' ? true : false;
});

5
src/server/api/wg-enable-expire-time.get.ts

@ -0,0 +1,5 @@
export default defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
const expires = WG_ENABLE_EXPIRES_TIME;
return expires === 'true' ? true : false;
});

5
src/server/api/wg-enable-one-time-links.get.ts

@ -0,0 +1,5 @@
export default defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
const otl = WG_ENABLE_ONE_TIME_LINKS;
return otl === 'true' ? true : false;
});

12
src/server/api/wireguard/client/[clientId]/expireDate.put.ts

@ -0,0 +1,12 @@
export default defineEventHandler(async (event) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
const { expireDate } = await readValidatedBody(
event,
validateZod(expireDateType)
);
await WireGuard.updateClientExpireDate({ clientId, expireDate });
return { success: true };
});

14
src/server/api/wireguard/client/[clientId]/generateOneTimeLink.post.ts

@ -0,0 +1,14 @@
export default defineEventHandler(async (event) => {
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
throw createError({
status: 404,
message: 'Invalid state',
});
}
const { clientId } = await getValidatedRouterParams(
event,
validateZod(clientIdType)
);
await WireGuard.generateOneTimeLink({ clientId });
return { success: true };
});

7
src/server/api/wireguard/client/index.post.ts

@ -1,5 +1,8 @@
export default defineEventHandler(async (event) => {
const { name } = await readValidatedBody(event, validateZod(nameType));
await WireGuard.createClient({ name });
const { name, expireDate } = await readValidatedBody(
event,
validateZod(createType)
);
await WireGuard.createClient({ name, expireDate });
return { success: true };
});

2
src/server/middleware/session.ts

@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
const authorization = getHeader(event, 'Authorization');
if (url.pathname.startsWith('/api/') && authorization) {
if (isPasswordValid(authorization)) {
if (isPasswordValid(authorization, PASSWORD_HASH)) {
return;
}
throw createError({

195
src/server/utils/WireGuard.ts

@ -3,6 +3,7 @@ import path from 'path';
import debug_logger from 'debug';
import crypto from 'node:crypto';
import QRCode from 'qrcode';
import CRC32 from 'crc-32';
const debug = debug_logger('WireGuard');
@ -13,6 +14,7 @@ type Server = {
};
type Client = {
id: string;
name: string;
address: string;
privateKey: string;
@ -20,8 +22,12 @@ type Client = {
preSharedKey: string;
createdAt: string;
updatedAt: string;
expireAt: string | null;
endpoint: string | null;
enabled: boolean;
allowedIPs?: never;
oneTimeLink: string | null;
oneTimeLinkExpiresAt: string | null;
};
type Config = {
@ -160,10 +166,14 @@ ${
publicKey: client.publicKey,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
expiredAt: client.expireAt !== null ? new Date(client.expireAt) : null,
allowedIPs: client.allowedIPs,
oneTimeLink: client.oneTimeLink ?? null,
oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null,
downloadableConfig: 'privateKey' in client,
persistentKeepalive: null as string | null,
latestHandshakeAt: null as Date | null,
endpoint: null as string | null,
transferRx: null as number | null,
transferTx: null as number | null,
})
@ -181,7 +191,7 @@ ${
const [
publicKey,
_preSharedKey,
_endpoint,
endpoint,
_allowedIps,
latestHandshakeAt,
transferRx,
@ -196,6 +206,7 @@ ${
latestHandshakeAt === '0'
? null
: new Date(Number(`${latestHandshakeAt}000`));
client.endpoint = endpoint === '(none)' ? null : (endpoint ?? null);
client.transferRx = Number(transferRx);
client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive ?? null;
@ -245,7 +256,13 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
}
async createClient({ name }: { name: string }) {
async createClient({
name,
expireDate,
}: {
name: string;
expireDate: string;
}) {
if (!name) {
throw new Error('Missing: Name');
}
@ -277,7 +294,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
// Create Client
const id = crypto.randomUUID();
const client = {
const client: Client = {
id,
name,
address,
@ -288,9 +305,21 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
endpoint: null,
oneTimeLink: null,
oneTimeLinkExpiresAt: null,
expireAt: null,
enabled: true,
};
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expireAt = date.toISOString();
}
config.clients[id] = client;
await this.saveConfig();
@ -317,6 +346,25 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await this.saveConfig();
}
async generateOneTimeLink({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
const key = `${clientId}-${Math.floor(Math.random() * 1000)}`;
client.oneTimeLink = Math.abs(CRC32.str(key)).toString(16);
client.oneTimeLinkExpiresAt = new Date(
Date.now() + 5 * 60 * 1000
).toISOString();
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async eraseOneTimeLink({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async disableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
@ -363,6 +411,29 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await this.saveConfig();
}
async updateClientExpireDate({
clientId,
expireDate,
}: {
clientId: string;
expireDate: string;
}) {
const client = await this.getClient({ clientId });
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expireAt = date.toISOString();
} else {
client.expireAt = null;
}
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async __reloadConfig() {
await this.__buildConfig();
await this.__syncConfig();
@ -389,6 +460,118 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
async Shutdown() {
await exec('wg-quick down wg0').catch(() => {});
}
async cronJobEveryMinute() {
const config = await this.getConfig();
let needSaveConfig = false;
// Expires Feature
if (WG_ENABLE_EXPIRES_TIME === 'true') {
for (const client of Object.values(config.clients)) {
if (client.enabled !== true) continue;
if (
client.expireAt !== null &&
new Date() > new Date(client.expireAt)
) {
debug(`Client ${client.id} expired.`);
needSaveConfig = true;
client.enabled = false;
client.updatedAt = new Date().toISOString();
}
}
}
// One Time Link Feature
if (WG_ENABLE_ONE_TIME_LINKS === 'true') {
for (const client of Object.values(config.clients)) {
if (
client.oneTimeLink !== null &&
client.oneTimeLinkExpiresAt !== null &&
new Date() > new Date(client.oneTimeLinkExpiresAt)
) {
debug(`Client ${client.id} One Time Link expired.`);
needSaveConfig = true;
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
}
}
}
if (needSaveConfig) {
await this.saveConfig();
}
}
async getMetrics() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
let wireguardSentBytes = '';
let wireguardReceivedBytes = '';
let wireguardLatestHandshakeSeconds = '';
for (const client of Object.values(clients)) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferTx)}\n`;
wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferRx)}\n`;
wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`;
}
let returnText = '# HELP wg-easy and wireguard metrics\n';
returnText += '\n# HELP wireguard_configured_peers\n';
returnText += '# TYPE wireguard_configured_peers gauge\n';
returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`;
returnText += '\n# HELP wireguard_enabled_peers\n';
returnText += '# TYPE wireguard_enabled_peers gauge\n';
returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`;
returnText += '\n# HELP wireguard_connected_peers\n';
returnText += '# TYPE wireguard_connected_peers gauge\n';
returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`;
returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n';
returnText += '# TYPE wireguard_sent_bytes counter\n';
returnText += `${wireguardSentBytes}`;
returnText +=
'\n# HELP wireguard_received_bytes Bytes received from the peer\n';
returnText += '# TYPE wireguard_received_bytes counter\n';
returnText += `${wireguardReceivedBytes}`;
returnText +=
'\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n';
returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n';
returnText += `${wireguardLatestHandshakeSeconds}`;
return returnText;
}
async getMetricsJSON() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
for (const client of Object.values(clients)) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
}
return {
wireguard_configured_peers: Number(wireguardPeerCount),
wireguard_enabled_peers: Number(wireguardEnabledPeersCount),
wireguard_connected_peers: Number(wireguardConnectedPeersCount),
};
}
}
const inst = new WireGuard();
@ -397,4 +580,10 @@ inst.getConfig().catch((err) => {
process.exit(1);
});
async function cronJobEveryMinute() {
await inst.cronJobEveryMinute();
setTimeout(cronJobEveryMinute, 60 * 1000);
}
cronJobEveryMinute();
export default inst;

15
src/server/utils/config.ts

@ -8,6 +8,9 @@ export const RELEASE = version;
export const PORT = process.env.PORT || '51821';
export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
export const PASSWORD_HASH = process.env.PASSWORD_HASH;
export const MAX_AGE = process.env.MAX_AGE
? parseInt(process.env.MAX_AGE, 10) * 1000 * 60
: 0;
export const WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
export const WG_DEVICE = process.env.WG_DEVICE || 'eth0';
export const WG_HOST = process.env.WG_HOST;
@ -50,12 +53,24 @@ iptables -D FORWARD -o wg0 -j ACCEPT;
export const LANG = process.env.LANG || 'en';
export const UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
export const UI_CHART_TYPE = process.env.UI_CHART_TYPE || '0';
export const WG_ENABLE_ONE_TIME_LINKS =
process.env.WG_ENABLE_ONE_TIME_LINKS || 'false';
export const UI_ENABLE_SORT_CLIENTS =
process.env.UI_ENABLE_SORT_CLIENTS || 'false';
export const WG_ENABLE_EXPIRES_TIME =
process.env.WG_ENABLE_EXPIRES_TIME || 'false';
export const ENABLE_PROMETHEUS_METRICS =
process.env.ENABLE_PROMETHEUS_METRICS || 'false';
export const PROMETHEUS_METRICS_PASSWORD =
process.env.PROMETHEUS_METRICS_PASSWORD;
export const REQUIRES_PASSWORD = !!PASSWORD_HASH;
export const REQUIRES_PROMETHEUS_PASSWORD = !!PROMETHEUS_METRICS_PASSWORD;
export const SESSION_CONFIG = {
password: getRandomHex(256),
name: 'wg-easy',
cookie: undefined,
} satisfies SessionConfig;
export const SERVER_DEBUG = debug('Server');

10
src/server/utils/password.ts

@ -8,13 +8,9 @@ import bcrypt from 'bcryptjs';
* @param {string} password String to test
* @returns {boolean} true if matching environment, otherwise false
*/
export function isPasswordValid(password: string): boolean {
if (typeof password !== 'string') {
return false;
}
if (PASSWORD_HASH) {
return bcrypt.compareSync(password, PASSWORD_HASH);
export function isPasswordValid(password: string, hash?: string): boolean {
if (hash) {
return bcrypt.compareSync(password, hash);
}
return false;

25
src/server/utils/types.ts

@ -30,6 +30,15 @@ const password = z
.string({ message: 'Password must be a valid string' })
.pipe(safeStringRefine);
const remember = z
.boolean({ message: 'Remember must be a valid boolean' })
.optional();
const expireDate = z
.string({ message: 'expiredDate must be a valid string' })
.min(1, 'expiredDate must be at least 1 Character')
.pipe(safeStringRefine);
export const clientIdType = z.object(
{
clientId: id,
@ -51,6 +60,21 @@ export const nameType = z.object(
{ message: 'Body must be a valid object' }
);
export const expireDateType = z.object(
{
expireDate: expireDate,
},
{ message: 'Body must be a valid object' }
);
export const createType = z.object(
{
name: name,
expireDate: expireDate,
},
{ message: 'Body must be a valid object' }
);
export const fileType = z.object(
{
file: file,
@ -61,6 +85,7 @@ export const fileType = z.object(
export const passwordType = z.object(
{
password: password,
remember: remember,
},
{ message: 'Body must be a valid object' }
);

Loading…
Cancel
Save