diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a30b7a1f..628ffb8d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,6 @@ # Copyright (c) Emile Nijssen (WeeJeWel) # Founder and Codeowner of WireGuard Easy (wg-easy) -# Maintained by Philip Heiduck (pheiduck) +# Maintained by Philip Heiduck (pheiduck) and Bernd Storath (kaaax0815) +* @WeeJeWel * @pheiduck +* @kaaax0815 diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml index 2a92e7b6..bd9b0f74 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -3,7 +3,7 @@ name: 🐛 Bug Report description: Create a report to help us improve title: "[Bug]: " labels: - - "bug" + - "type: bug" body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml index d23b5f6c..b8c1f914 100644 --- a/.github/ISSUE_TEMPLATE/02-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml @@ -3,7 +3,7 @@ name: 🛠️ Feature Request description: Suggest an idea to help us improve title: "[Feat]: " labels: - - "enhancement" + - "type: feature request" body: - type: markdown diff --git a/README.md b/README.md index a282d2d6..37b158a7 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!

## Features + * All-in-one: WireGuard + Web UI. * Easy installation, simple to use. * 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. * Automatic Light / Dark Mode * Multilanguage Support -* UI_TRAFFIC_STATS (default off) -* UI_SHOW_LINKS (default off) -* WG_ENABLE_EXPIRES_TIME (default off) +* Traffic Stats (default off) +* One Time Links (default off) +* Client Expiry (default off) +* Prometheus metrics support ## 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 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` WireGuard Easy can be launched with Docker Compose as well - just download @@ -109,7 +113,7 @@ These options can be configured by setting environment variables using `-e KEY=" | `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. | | `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. | | `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. | -| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) +| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) | `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. | | `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. | | `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. | @@ -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). | | `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_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. | | `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. diff --git a/docker-compose.yml b/docker-compose.yml index 9de55e32..095557bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,9 +27,11 @@ services: # - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt # - UI_TRAFFIC_STATS=true # - 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 # - 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 container_name: wg-easy diff --git a/src/config.js b/src/config.js index 6168e589..01f0ce20 100644 --- a/src/config.js +++ b/src/config.js @@ -38,6 +38,8 @@ iptables -D FORWARD -o wg0 -j ACCEPT; module.exports.LANG = process.env.LANG || 'en'; 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_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.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; diff --git a/src/lib/Server.js b/src/lib/Server.js index 40b9bb1f..17e5058b 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -2,6 +2,7 @@ const bcrypt = require('bcryptjs'); const crypto = require('node:crypto'); +const basicAuth = require('basic-auth'); const { createServer } = require('node:http'); const { stat, readFile } = require('node:fs/promises'); const { resolve, sep } = require('node:path'); @@ -33,12 +34,15 @@ const { LANG, UI_TRAFFIC_STATS, UI_CHART_TYPE, - UI_SHOW_LINKS, + WG_ENABLE_ONE_TIME_LINKS, UI_ENABLE_SORT_CLIENTS, WG_ENABLE_EXPIRES_TIME, + ENABLE_PROMETHEUS_METRICS, + PROMETHEUS_METRICS_PASSWORD, } = require('../config'); const requiresPassword = !!PASSWORD_HASH; +const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD; /** * Checks if `password` matches the PASSWORD_HASH. @@ -48,13 +52,12 @@ const requiresPassword = !!PASSWORD_HASH; * @param {string} password String to test * @returns {boolean} true if matching environment, otherwise false */ -const isPasswordValid = (password) => { +const isPasswordValid = (password, hash) => { if (typeof password !== 'string') { return false; } - - if (PASSWORD_HASH) { - return bcrypt.compareSync(password, PASSWORD_HASH); + if (hash) { + return bcrypt.compareSync(password, hash); } return false; @@ -106,9 +109,9 @@ module.exports = class Server { 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'); - return `${UI_SHOW_LINKS}`; + return `${WG_ENABLE_ONE_TIME_LINKS}`; })) .get('/api/ui-sort-clients', defineEventHandler((event) => { @@ -132,14 +135,21 @@ module.exports = class Server { authenticated, }; })) - .get('/:clientHash', defineEventHandler(async (event) => { - const clientHash = getRouterParam(event, 'clientHash'); + .get('/cnf/:clientOneTimeLink', 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.hash === clientHash); + const client = clients.find((client) => client.oneTimeLink === clientOneTimeLink); if (!client) return; const clientId = client.id; 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'); return config; })) @@ -155,7 +165,7 @@ module.exports = class Server { }); } - if (!isPasswordValid(password)) { + if (!isPasswordValid(password, PASSWORD_HASH)) { throw createError({ status: 401, message: 'Incorrect Password', @@ -185,7 +195,7 @@ module.exports = class Server { } if (req.url.startsWith('/api/') && req.headers['authorization']) { - if (isPasswordValid(req.headers['authorization'])) { + if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) { return next(); } return res.status(401).json({ @@ -252,6 +262,20 @@ module.exports = class Server { await WireGuard.enableClient({ clientId }); 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) => { const clientId = getRouterParam(event, 'clientId'); 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 const router3 = createRouter(); app.use(router3); diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js index 120dd238..1aaed9a3 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -25,6 +25,7 @@ const { WG_PRE_DOWN, WG_POST_DOWN, WG_ENABLE_EXPIRES_TIME, + WG_ENABLE_ONE_TIME_LINKS, } = require('../config'); module.exports = class WireGuard { @@ -152,12 +153,14 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' ? new Date(client.expiredAt) : null, allowedIPs: client.allowedIPs, - hash: Math.abs(CRC32.str(clientId)).toString(16), + oneTimeLink: client.oneTimeLink ?? null, + oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null, downloadableConfig: 'privateKey' in client, persistentKeepalive: null, latestHandshakeAt: null, transferRx: null, transferTx: null, + endpoint: null, })); // Loop WireGuard status @@ -186,6 +189,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' client.latestHandshakeAt = latestHandshakeAt === '0' ? null : new Date(Number(`${latestHandshakeAt}000`)); + client.endpoint = endpoint === '(none)' ? null : endpoint; client.transferRx = Number(transferRx); client.transferTx = Number(transferTx); client.persistentKeepalive = persistentKeepalive; @@ -306,6 +310,23 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; 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 }) { const client = await this.getClient({ clientId }); @@ -381,8 +402,9 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; async cronJobEveryMinute() { const config = await this.getConfig(); + let needSaveConfig = false; + // Expires Feature if (WG_ENABLE_EXPIRES_TIME === 'true') { - let needSaveConfig = false; for (const client of Object.values(config.clients)) { if (client.enabled !== true) continue; if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) { @@ -392,10 +414,93 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; 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), + }; } }; diff --git a/src/package-lock.json b/src/package-lock.json index 09668cd6..b05c4f83 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.1", "license": "CC BY-NC-SA 4.0", "dependencies": { + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "crc-32": "^1.2.2", "debug": "^4.3.6", @@ -992,6 +993,18 @@ "dev": true, "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": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -2186,6 +2199,26 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "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": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2849,9 +2882,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4245,23 +4278,9 @@ } }, "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" - } - ], + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/safe-regex-test": { diff --git a/src/package.json b/src/package.json index b0032c2a..007f08c2 100644 --- a/src/package.json +++ b/src/package.json @@ -15,6 +15,7 @@ "author": "Emile Nijssen", "license": "CC BY-NC-SA 4.0", "dependencies": { + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "crc-32": "^1.2.2", "debug": "^4.3.6", diff --git a/src/www/index.html b/src/www/index.html index a611e9c0..a35e81ef 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -256,8 +256,8 @@ {{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}} -
- {{document.location.protocol}}//{{document.location.host}}/{{client.hash}} +
+ {{document.location.protocol}}//{{document.location.host}}/cnf/{{client.oneTimeLink}}
@@ -384,6 +384,22 @@ + + +