Browse Source

Merge branch 'wg-easy:master' into master

pull/1319/head
Vadim Babadzhanyan 2 years ago
committed by GitHub
parent
commit
e9548d23d3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .github/CODEOWNERS
  2. 2
      .github/ISSUE_TEMPLATE/01-bug-report.yml
  3. 2
      .github/ISSUE_TEMPLATE/02-feature-request.yml
  4. 15
      README.md
  5. 4
      docker-compose.yml
  6. 4
      src/config.js
  7. 95
      src/lib/Server.js
  8. 113
      src/lib/WireGuard.js
  9. 59
      src/package-lock.json
  10. 1
      src/package.json
  11. 20
      src/www/index.html
  12. 11
      src/www/js/api.js
  13. 13
      src/www/js/app.js
  14. 2
      src/www/js/i18n.js

4
.github/CODEOWNERS

@ -1,4 +1,6 @@
# Copyright (c) Emile Nijssen (WeeJeWel) # Copyright (c) Emile Nijssen (WeeJeWel)
# Founder and Codeowner of WireGuard Easy (wg-easy) # Founder and Codeowner of WireGuard Easy (wg-easy)
# Maintained by Philip Heiduck (pheiduck) # Maintained by Philip Heiduck (pheiduck) and Bernd Storath (kaaax0815)
* @WeeJeWel
* @pheiduck * @pheiduck
* @kaaax0815

2
.github/ISSUE_TEMPLATE/01-bug-report.yml

@ -3,7 +3,7 @@ name: 🐛 Bug Report
description: Create a report to help us improve description: Create a report to help us improve
title: "[Bug]: " title: "[Bug]: "
labels: labels:
- "bug" - "type: bug"
body: body:
- type: markdown - type: markdown

2
.github/ISSUE_TEMPLATE/02-feature-request.yml

@ -3,7 +3,7 @@ name: 🛠️ Feature Request
description: Suggest an idea to help us improve description: Suggest an idea to help us improve
title: "[Feat]: " title: "[Feat]: "
labels: labels:
- "enhancement" - "type: feature request"
body: body:
- type: markdown - type: markdown

15
README.md

@ -13,6 +13,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
</p> </p>
## Features ## Features
* All-in-one: WireGuard + Web UI. * All-in-one: WireGuard + Web UI.
* Easy installation, simple to use. * Easy installation, simple to use.
* List, create, edit, delete, enable & disable clients. * List, create, edit, delete, enable & disable clients.
@ -23,9 +24,10 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
* Gravatar support. * Gravatar support.
* Automatic Light / Dark Mode * Automatic Light / Dark Mode
* Multilanguage Support * Multilanguage Support
* UI_TRAFFIC_STATS (default off) * Traffic Stats (default off)
* UI_SHOW_LINKS (default off) * One Time Links (default off)
* WG_ENABLE_EXPIRES_TIME (default off) * Client Expiry (default off)
* Prometheus metrics support
## Requirements ## Requirements
@ -87,6 +89,8 @@ To automatically install & run wg-easy, simply run:
The Web UI will now be available on `http://0.0.0.0:51821`. The Web UI will now be available on `http://0.0.0.0:51821`.
The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/)
> 💡 Your configuration files will be saved in `~/.wg-easy` > 💡 Your configuration files will be saved in `~/.wg-easy`
WireGuard Easy can be launched with Docker Compose as well - just download WireGuard Easy can be launched with Docker Compose as well - just download
@ -123,10 +127,11 @@ These options can be configured by setting environment variables using `-e KEY="
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). | | `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI | | `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart | | `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
| `UI_SHOW_LINKS` | `false` | `true` | Enable display of a short download link in Web UI | | `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) |
| `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. | | `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. |
| `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name | | `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name |
| `ENABLE_PROMETHEUS_METRICS` | `false` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`|
| `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
> If you change `WG_PORT`, make sure to also change the exposed port. > If you change `WG_PORT`, make sure to also change the exposed port.

4
docker-compose.yml

@ -27,9 +27,11 @@ services:
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt # - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
# - UI_TRAFFIC_STATS=true # - UI_TRAFFIC_STATS=true
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart) # - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
# - UI_SHOW_LINKS=true # - WG_ENABLE_ONE_TIME_LINKS=true
# - UI_ENABLE_SORT_CLIENTS=true # - UI_ENABLE_SORT_CLIENTS=true
# - WG_ENABLE_EXPIRES_TIME=true # - WG_ENABLE_EXPIRES_TIME=true
# - ENABLE_PROMETHEUS_METRICS=false
# - PROMETHEUS_METRICS_PASSWORD=$$2a$$12$$vkvKpeEAHD78gasyawIod.1leBMKg8sBwKW.pQyNsq78bXV3INf2G # (needs double $$, hash of 'prometheus_password'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
image: ghcr.io/wg-easy/wg-easy image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy container_name: wg-easy

4
src/config.js

@ -38,6 +38,8 @@ iptables -D FORWARD -o wg0 -j ACCEPT;
module.exports.LANG = process.env.LANG || 'en'; module.exports.LANG = process.env.LANG || 'en';
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false'; module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
module.exports.UI_SHOW_LINKS = process.env.UI_SHOW_LINKS || 'false'; module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS || 'false';
module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false'; module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false';
module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false'; module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false';
module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'false';
module.exports.PROMETHEUS_METRICS_PASSWORD = process.env.PROMETHEUS_METRICS_PASSWORD;

95
src/lib/Server.js

@ -2,6 +2,7 @@
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const basicAuth = require('basic-auth');
const { createServer } = require('node:http'); const { createServer } = require('node:http');
const { stat, readFile } = require('node:fs/promises'); const { stat, readFile } = require('node:fs/promises');
const { resolve, sep } = require('node:path'); const { resolve, sep } = require('node:path');
@ -33,12 +34,15 @@ const {
LANG, LANG,
UI_TRAFFIC_STATS, UI_TRAFFIC_STATS,
UI_CHART_TYPE, UI_CHART_TYPE,
UI_SHOW_LINKS, WG_ENABLE_ONE_TIME_LINKS,
UI_ENABLE_SORT_CLIENTS, UI_ENABLE_SORT_CLIENTS,
WG_ENABLE_EXPIRES_TIME, WG_ENABLE_EXPIRES_TIME,
ENABLE_PROMETHEUS_METRICS,
PROMETHEUS_METRICS_PASSWORD,
} = require('../config'); } = require('../config');
const requiresPassword = !!PASSWORD_HASH; const requiresPassword = !!PASSWORD_HASH;
const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD;
/** /**
* Checks if `password` matches the PASSWORD_HASH. * Checks if `password` matches the PASSWORD_HASH.
@ -48,13 +52,12 @@ const requiresPassword = !!PASSWORD_HASH;
* @param {string} password String to test * @param {string} password String to test
* @returns {boolean} true if matching environment, otherwise false * @returns {boolean} true if matching environment, otherwise false
*/ */
const isPasswordValid = (password) => { const isPasswordValid = (password, hash) => {
if (typeof password !== 'string') { if (typeof password !== 'string') {
return false; return false;
} }
if (hash) {
if (PASSWORD_HASH) { return bcrypt.compareSync(password, hash);
return bcrypt.compareSync(password, PASSWORD_HASH);
} }
return false; return false;
@ -106,9 +109,9 @@ module.exports = class Server {
return `"${UI_CHART_TYPE}"`; return `"${UI_CHART_TYPE}"`;
})) }))
.get('/api/ui-show-links', defineEventHandler((event) => { .get('/api/wg-enable-one-time-links', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json'); setHeader(event, 'Content-Type', 'application/json');
return `${UI_SHOW_LINKS}`; return `${WG_ENABLE_ONE_TIME_LINKS}`;
})) }))
.get('/api/ui-sort-clients', defineEventHandler((event) => { .get('/api/ui-sort-clients', defineEventHandler((event) => {
@ -132,14 +135,21 @@ module.exports = class Server {
authenticated, authenticated,
}; };
})) }))
.get('/:clientHash', defineEventHandler(async (event) => { .get('/cnf/:clientOneTimeLink', defineEventHandler(async (event) => {
const clientHash = getRouterParam(event, 'clientHash'); 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 clients = await WireGuard.getClients();
const client = clients.find((client) => client.hash === clientHash); const client = clients.find((client) => client.oneTimeLink === clientOneTimeLink);
if (!client) return; if (!client) return;
const clientId = client.id; const clientId = client.id;
const config = await WireGuard.getClientConfiguration({ clientId }); const config = await WireGuard.getClientConfiguration({ clientId });
setHeader(event, 'Content-Disposition', `attachment; filename="${clientHash}.conf"`); await WireGuard.eraseOneTimeLink({ clientId });
setHeader(event, 'Content-Disposition', `attachment; filename="${clientOneTimeLink}.conf"`);
setHeader(event, 'Content-Type', 'text/plain'); setHeader(event, 'Content-Type', 'text/plain');
return config; return config;
})) }))
@ -155,7 +165,7 @@ module.exports = class Server {
}); });
} }
if (!isPasswordValid(password)) { if (!isPasswordValid(password, PASSWORD_HASH)) {
throw createError({ throw createError({
status: 401, status: 401,
message: 'Incorrect Password', message: 'Incorrect Password',
@ -185,7 +195,7 @@ module.exports = class Server {
} }
if (req.url.startsWith('/api/') && req.headers['authorization']) { if (req.url.startsWith('/api/') && req.headers['authorization']) {
if (isPasswordValid(req.headers['authorization'])) { if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) {
return next(); return next();
} }
return res.status(401).json({ return res.status(401).json({
@ -252,6 +262,20 @@ module.exports = class Server {
await WireGuard.enableClient({ clientId }); await WireGuard.enableClient({ clientId });
return { success: true }; return { success: true };
})) }))
.post('/api/wireguard/client/:clientId/generateOneTimeLink', defineEventHandler(async (event) => {
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
throw createError({
status: 404,
message: 'Invalid state',
});
}
const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
throw createError({ status: 403 });
}
await WireGuard.generateOneTimeLink({ clientId });
return { success: true };
}))
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => { .post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
const clientId = getRouterParam(event, 'clientId'); const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
@ -311,6 +335,51 @@ module.exports = class Server {
}); });
}; };
// Prometheus Metrics API
const routerPrometheusMetrics = createRouter();
app.use(routerPrometheusMetrics);
// Check Prometheus credentials
app.use(
fromNodeMiddleware((req, res, next) => {
if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) {
return next();
}
const user = basicAuth(req);
if (requiresPrometheusPassword && !user) {
res.statusCode = 401;
return { error: 'Not Logged In' };
}
if (user.pass) {
if (isPasswordValid(user.pass, PROMETHEUS_METRICS_PASSWORD)) {
return next();
}
res.statusCode = 401;
return { error: 'Incorrect Password' };
}
res.statusCode = 401;
return { error: 'Not Logged In' };
}),
);
// Prometheus Routes
routerPrometheusMetrics
.get('/metrics', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'text/plain');
if (ENABLE_PROMETHEUS_METRICS === 'true') {
return WireGuard.getMetrics();
}
return '';
}))
.get('/metrics/json', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json');
if (ENABLE_PROMETHEUS_METRICS === 'true') {
return WireGuard.getMetricsJSON();
}
return '';
}));
// backup_restore // backup_restore
const router3 = createRouter(); const router3 = createRouter();
app.use(router3); app.use(router3);

113
src/lib/WireGuard.js

@ -25,6 +25,7 @@ const {
WG_PRE_DOWN, WG_PRE_DOWN,
WG_POST_DOWN, WG_POST_DOWN,
WG_ENABLE_EXPIRES_TIME, WG_ENABLE_EXPIRES_TIME,
WG_ENABLE_ONE_TIME_LINKS,
} = require('../config'); } = require('../config');
module.exports = class WireGuard { module.exports = class WireGuard {
@ -152,12 +153,14 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
? new Date(client.expiredAt) ? new Date(client.expiredAt)
: null, : null,
allowedIPs: client.allowedIPs, allowedIPs: client.allowedIPs,
hash: Math.abs(CRC32.str(clientId)).toString(16), oneTimeLink: client.oneTimeLink ?? null,
oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null,
downloadableConfig: 'privateKey' in client, downloadableConfig: 'privateKey' in client,
persistentKeepalive: null, persistentKeepalive: null,
latestHandshakeAt: null, latestHandshakeAt: null,
transferRx: null, transferRx: null,
transferTx: null, transferTx: null,
endpoint: null,
})); }));
// Loop WireGuard status // Loop WireGuard status
@ -186,6 +189,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
client.latestHandshakeAt = latestHandshakeAt === '0' client.latestHandshakeAt = latestHandshakeAt === '0'
? null ? null
: new Date(Number(`${latestHandshakeAt}000`)); : new Date(Number(`${latestHandshakeAt}000`));
client.endpoint = endpoint === '(none)' ? null : endpoint;
client.transferRx = Number(transferRx); client.transferRx = Number(transferRx);
client.transferTx = Number(transferTx); client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive; client.persistentKeepalive = persistentKeepalive;
@ -306,6 +310,23 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await this.saveConfig(); await this.saveConfig();
} }
async generateOneTimeLink({ clientId }) {
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);
client.updatedAt = new Date();
await this.saveConfig();
}
async eraseOneTimeLink({ clientId }) {
const client = await this.getClient({ clientId });
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date();
await this.saveConfig();
}
async disableClient({ clientId }) { async disableClient({ clientId }) {
const client = await this.getClient({ clientId }); const client = await this.getClient({ clientId });
@ -381,8 +402,9 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
async cronJobEveryMinute() { async cronJobEveryMinute() {
const config = await this.getConfig(); const config = await this.getConfig();
let needSaveConfig = false;
// Expires Feature
if (WG_ENABLE_EXPIRES_TIME === 'true') { if (WG_ENABLE_EXPIRES_TIME === 'true') {
let needSaveConfig = false;
for (const client of Object.values(config.clients)) { for (const client of Object.values(config.clients)) {
if (client.enabled !== true) continue; if (client.enabled !== true) continue;
if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) { if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) {
@ -392,10 +414,93 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
client.updatedAt = new Date(); client.updatedAt = new Date();
} }
} }
if (needSaveConfig) { }
await this.saveConfig(); // One Time Link Feature
if (WG_ENABLE_ONE_TIME_LINKS === 'true') {
for (const client of Object.values(config.clients)) {
if (client.oneTimeLink !== 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();
}
}
}
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),
};
} }
}; };

59
src/package-lock.json

@ -9,6 +9,7 @@
"version": "1.0.1", "version": "1.0.1",
"license": "CC BY-NC-SA 4.0", "license": "CC BY-NC-SA 4.0",
"dependencies": { "dependencies": {
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"debug": "^4.3.6", "debug": "^4.3.6",
@ -992,6 +993,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bcryptjs": { "node_modules/bcryptjs": {
"version": "2.4.3", "version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@ -2186,6 +2199,26 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/express-session/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -2849,9 +2882,9 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.15.0", "version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4245,23 +4278,9 @@
} }
}, },
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-regex-test": { "node_modules/safe-regex-test": {

1
src/package.json

@ -15,6 +15,7 @@
"author": "Emile Nijssen", "author": "Emile Nijssen",
"license": "CC BY-NC-SA 4.0", "license": "CC BY-NC-SA 4.0",
"dependencies": { "dependencies": {
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"debug": "^4.3.6", "debug": "^4.3.6",

20
src/www/index.html

@ -256,8 +256,8 @@
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}} {{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
</span> </span>
</div> </div>
<div v-if="uiShowLinks" :ref="'client-' + client.id + '-hash'" class="text-gray-400 text-xs"> <div v-if="enableOneTimeLinks && client.oneTimeLink !== null && client.oneTimeLink !== ''" :ref="'client-' + client.id + '-link'" class="text-gray-400 text-xs">
<a :href="'./' + client.hash + ''">{{document.location.protocol}}//{{document.location.host}}/{{client.hash}}</a> <a :href="'./cnf/' + client.oneTimeLink + ''">{{document.location.protocol}}//{{document.location.host}}/cnf/{{client.oneTimeLink}}</a>
</div> </div>
<!-- Expire Date --> <!-- Expire Date -->
<div v-show="enableExpireTime" class=" block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs"> <div v-show="enableExpireTime" class=" block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs">
@ -384,6 +384,22 @@
</svg> </svg>
</a> </a>
<!-- Short OneTime Link -->
<button v-if="enableOneTimeLinks" :disabled="!client.downloadableConfig"
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition"
:class="{
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig,
'is-disabled': !client.downloadableConfig
}"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('OneTimeLink')"
@click="if(client.downloadableConfig) { showOneTimeLink(client); }">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961"/>
</svg>
</button>
<!-- Delete --> <!-- Delete -->
<button <button

11
src/www/js/api.js

@ -64,10 +64,10 @@ class API {
}); });
} }
async getUIShowLinks() { async getWGEnableOneTimeLinks() {
return this.call({ return this.call({
method: 'get', method: 'get',
path: '/ui-show-links', path: '/wg-enable-one-time-links',
}); });
} }
@ -132,6 +132,13 @@ class API {
}); });
} }
async showOneTimeLink({ clientId }) {
return this.call({
method: 'post',
path: `/wireguard/client/${clientId}/generateOneTimeLink`,
});
}
async enableClient({ clientId }) { async enableClient({ clientId }) {
return this.call({ return this.call({
method: 'post', method: 'post',

13
src/www/js/app.js

@ -92,7 +92,7 @@ new Vue({
uiTrafficStats: false, uiTrafficStats: false,
uiChartType: 0, uiChartType: 0,
uiShowLinks: false, enableOneTimeLinks: false,
enableSortClient: false, enableSortClient: false,
sortClient: true, // Sort clients by name, true = asc, false = desc sortClient: true, // Sort clients by name, true = asc, false = desc
enableExpireTime: false, enableExpireTime: false,
@ -312,6 +312,11 @@ new Vue({
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error)); .finally(() => this.refresh().catch(console.error));
}, },
showOneTimeLink(client) {
this.api.showOneTimeLink({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
enableClient(client) { enableClient(client) {
this.api.enableClient({ clientId: client.id }) this.api.enableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
@ -436,12 +441,12 @@ new Vue({
this.uiChartType = 0; this.uiChartType = 0;
}); });
this.api.getUIShowLinks() this.api.getWGEnableOneTimeLinks()
.then((res) => { .then((res) => {
this.uiShowLinks = res; this.enableOneTimeLinks = res;
}) })
.catch(() => { .catch(() => {
this.uiShowLinks = false; this.enableOneTimeLinks = false;
}); });
this.api.getUiSortClients() this.api.getUiSortClients()

2
src/www/js/i18n.js

@ -39,6 +39,7 @@ const messages = { // eslint-disable-line no-unused-vars
sort: 'Sort', sort: 'Sort',
ExpireDate: 'Expire Date', ExpireDate: 'Expire Date',
Permanent: 'Permanent', Permanent: 'Permanent',
OneTimeLink: 'Generate short one time link',
}, },
ua: { ua: {
name: 'Ім`я', name: 'Ім`я',
@ -112,6 +113,7 @@ const messages = { // eslint-disable-line no-unused-vars
sort: 'Сортировка', sort: 'Сортировка',
ExpireDate: 'Дата истечения срока', ExpireDate: 'Дата истечения срока',
Permanent: 'Бессрочно', Permanent: 'Бессрочно',
OneTimeLink: 'Создать короткую одноразовую ссылку',
}, },
tr: { // Müslüm Barış Korkmazer @babico tr: { // Müslüm Barış Korkmazer @babico
name: 'İsim', name: 'İsim',

Loading…
Cancel
Save