Browse Source

Adapt code AmneziaWG v14+fix Lint

pull/2249/head
Andre 9 months ago
parent
commit
f6fdd2e1ec
  1. 23
      .editorconfig
  2. 11
      .env
  3. 10
      .github/workflows/deploy-pr.yml
  4. 10
      .github/workflows/deploy.yml
  5. 98
      .github/workflows/docker-publish.yml
  6. 4
      .github/workflows/lint.yml
  7. 1
      .gitignore
  8. 16
      Dockerfile
  9. 59
      How_to_generate_an_bcrypt_hash.md
  10. 128
      README.md
  11. BIN
      assets/awg-easy.sketch
  12. BIN
      assets/screenshot.png
  13. BIN
      assets/wg-easy.sketch
  14. 4
      awg-easy.service
  15. 101
      contributing.md
  16. 4
      docker-compose.dev.yml
  17. 43
      docker-compose.yml
  18. 6
      package-lock.json
  19. 12
      package.json
  20. 27
      src/config.js
  21. 157
      src/lib/Server.js
  22. 196
      src/lib/WireGuard.js
  23. 202
      src/package-lock.json
  24. 13
      src/package.json
  25. 30
      src/wgpw.mjs
  26. 14
      src/www/css/app.css
  27. BIN
      src/www/img/apple-touch-icon.png
  28. BIN
      src/www/img/favicon.ico
  29. BIN
      src/www/img/favicon.png
  30. BIN
      src/www/img/logo.png
  31. 33
      src/www/img/logo.svg
  32. 109
      src/www/index.html
  33. 61
      src/www/js/api.js
  34. 104
      src/www/js/app.js
  35. 53
      src/www/js/i18n.js
  36. 6
      src/www/manifest.json
  37. 4
      src/www/src/css/app.css

23
.editorconfig

@ -0,0 +1,23 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# The JSON files contain newlines inconsistently
[*.json]
insert_final_newline = ignore
# Minified JavaScript files shouldn't be changed
[**.min.js]
indent_style = ignore
insert_final_newline = ignore
[*.md]
trim_trailing_whitespace = false

11
.env

@ -0,0 +1,11 @@
WG_HOST=🚨YOUR_SERVER_IP
# (Supports: en, ru, tr, no, pl, fr, de, ca, es)
LANGUAGE=en
PORT=51821
WG_DEVICE=eth0
WG_PORT=51820
WG_DEFAULT_ADDRESS=10.8.0.x
WG_DEFAULT_DNS=1.1.1.1
WG_ALLOWED_IPS=0.0.0.0/0, ::/0
DICEBEAR_TYPE=bottts
USE_GRAVATAR=true

10
.github/workflows/deploy-pr.yml

@ -7,21 +7,21 @@ jobs:
deploy:
name: Build & Deploy
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
if: github.repository_owner == 'evoll'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3.11.1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -32,4 +32,4 @@ jobs:
with:
push: false
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ghcr.io/wg-easy/wg-easy:pr
tags: ghcr.io/evoll/awg-easy:pr

10
.github/workflows/deploy.yml

@ -10,13 +10,13 @@ jobs:
name: Build & Deploy
runs-on: ubuntu-latest
if: |
github.repository_owner == 'wg-easy' &&
github.repository_owner == 'evoll' &&
!contains(github.event.head_commit.message, '!skipci')
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: v14
@ -24,10 +24,10 @@ jobs:
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3.11.1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -38,4 +38,4 @@ jobs:
with:
push: true
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ghcr.io/wg-easy/wg-easy:latest, ghcr.io/wg-easy/wg-easy:14
tags: ghcr.io/evoll/awg-easy:14

98
.github/workflows/docker-publish.yml

@ -0,0 +1,98 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '43 21 * * *'
push:
branches: [ "v14" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "v14" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad #v4.0.0
with:
cosign-release: 'v2.2.4'
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@1583c0f09d26c58c59d25b0eef29792b7ce99d9a # v3.0.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28fdb31ff34708d19615a74d67103ddc2ea9725c # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

4
.github/workflows/lint.yml

@ -10,10 +10,10 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
if: github.repository_owner == 'wg-easy'
if: github.repository_owner == 'evoll'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: actions/setup-node@v4
with:

1
.gitignore

@ -4,3 +4,4 @@
/src/node_modules
.DS_Store
*.swp
.idea

16
Dockerfile

@ -1,9 +1,9 @@
# As a workaround we have to build on nodejs 18
# nodejs 20 hangs on build with armv6/armv7
FROM docker.io/library/node:lts-alpine AS build_node_modules
FROM docker.io/library/node:18-alpine AS build_node_modules
# Update npm to latest
RUN npm install -g npm@latest
RUN npm install -g npm@10.8.2
# Copy Web UI
COPY src /app
@ -13,8 +13,8 @@ RUN npm ci --omit=dev &&\
# Copy build result to a new image.
# This saves a lot of disk space.
FROM docker.io/library/node:lts-alpine
HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3
FROM amneziavpn/amnezia-wg:latest
HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/awg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3
COPY --from=build_node_modules /app /app
# Move node_modules one directory up, so during development
@ -35,15 +35,15 @@ RUN apk add --no-cache \
dpkg \
dumb-init \
iptables \
iptables-legacy \
wireguard-tools
nodejs \
npm
# Use iptables-legacy
RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save
# Set Environment
ENV DEBUG=Server,WireGuard
# Run Web UI
WORKDIR /app
CMD ["/usr/bin/dumb-init", "node", "server.js"]
CMD ["/usr/bin/dumb-init", "node", "server.js"]

59
How_to_generate_an_bcrypt_hash.md

@ -1,45 +1,42 @@
# Generating bcrypt-hashed password
# wg-password
With version 14 of wg-easy, a password hashed with bcrypt is needed instead of the plain-text password string. This doc explains how to generate the hash based on a plain-text password.
`wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `awg-easy`, enhancing security by requiring passwords.
## Using Docker + node
## Features
- You are using docker compose
- Generate bcrypt password hashes.
- Easily integrate with `awg-easy` to enforce password requirements.
The easiest way to generate a bcrypt password hash with wgpw is using docker and node:
## Usage with Docker
```sh
docker run ghcr.io/wg-easy/wg-easy:14 node -e 'const bcrypt = require("bcryptjs"); const hash = bcrypt.hashSync("YOUR_PASSWORD", 10); console.log(hash.replace(/\$/g, "$$$$"));'
```
The hashed password will get printed on your terminal. Copy it and use on the `PASSWORD_HASH` environment variable in your docker compose.
- You are using `docker run`
If you are using `docker run` for running wg-easy, you must enclose the hash string in single quotes (`'...'`). You can use this command:
```sh
docker run --rm ghcr.io/wg-easy/wg-easy:14 node -e "const bcrypt = require('bcryptjs'); const hash = bcrypt.hashSync('YOUR_PASSWORD', 10); console.log('\'' + hash + '\'');"
```
The hashed password will get printed on your terminal. Copy it and use on the `PASSWORD_HASH` environment variable in your docker run command.
## Using Docker + wgpw
`wg-password` (wgpw) is a script that generates bcrypt password hashes. You can use it with docker:
To generate a bcrypt password hash using docker, run the following command :
```sh
docker run ghcr.io/wg-easy/wg-easy:14 wgpw YOUR_PASSWORD
docker run -it ghcr.io/evoll/awg-easy:14 wgpw YOUR_PASSWORD
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
```
You will see an output similar to this:
If a password is not provided, the tool will prompt you for one :
```sh
docker run -it ghcr.io/evoll/awg-easy:14 wgpw
Enter your password: // hidden prompt, type in your password
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW'
```
In this example, the `$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW` string is your hashed password. For using it with docker-compose, you need to escape each `$` characters by adding another `$` before them, or they will get interpreted as variables. The final password you can use in docker-compose will look like this:
**Important** : make sure to enclose your password in **single quotes** when you run `docker run` command :
```sh
$$2b$$12$$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW
```bash
$ echo $2b$12$coPqCsPtcF <-- not correct
b2
$ echo "$2b$12$coPqCsPtcF" <-- not correct
b2
$ echo '$2b$12$coPqCsPtcF' <-- correct
$2b$12$coPqCsPtcF
```
**Important** : Please note: don't wrap the generated hash password in single quotes when you use `docker-compose.yml`. Instead, replace each `$` symbol with two `$$` symbols. For example:
``` yaml
- PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG
```
This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/evoll/awg-easy:14 wgpw foobar123` and then inserted an additional `$` before each existing `$` symbol.

128
README.md

@ -1,40 +1,32 @@
# WireGuard Easy
# AmnewziaWG Easy
[![Build & Publish Docker Image to Docker Hub](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml/badge.svg?branch=production)](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml)
[![Lint](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml)
![Docker](https://img.shields.io/docker/pulls/weejewel/wg-easy.svg)
[![Sponsor](https://img.shields.io/github/sponsors/weejewel)](https://github.com/sponsors/WeeJeWel)
![GitHub Stars](https://img.shields.io/github/stars/wg-easy/wg-easy)
You have found the easiest way to install & manage WireGuard on any Linux host!
You have found the easiest way to install & manage AmnewziaWG on any Linux host!
<p align="center">
<img src="./assets/screenshot.png" width="802" />
</p>
## Features
* All-in-one: WireGuard + Web UI.
* All-in-one: AmneziaWG + Web UI.
* Easy installation, simple to use.
* List, create, edit, delete, enable & disable clients.
* Show a client's QR code.
* Download a client's configuration file.
* Statistics for which clients are connected.
* Tx/Rx charts for each connected client.
* Gravatar support.
* Gravatar support or random avatars.
* Automatic Light / Dark Mode
* Multilanguage Support
* UI_TRAFFIC_STATS (default off)
* Traffic Stats (default off)
* One Time Links (default off)
* Client Expiry (default off)
* Prometheus metrics support
## Requirements
* A host with a kernel that supports WireGuard (all modern kernels).
* A host with Docker installed.
## Versions
This branch is only for the v14 release of WireGuard Easy.
For newer versions, please refer to the [master branch](https://github.com/wg-easy/wg-easy/tree/master).
## Installation
### 1. Install Docker
@ -49,70 +41,83 @@ exit
And log in again.
### 2. Run WireGuard Easy
### 2. Run AmneziaWG Easy
To automatically install & run wg-easy, simply run:
To automatically install & run awg-easy, simply run:
```
docker run -d \
--name=wg-easy \
-e LANG=de \
--name=awg-easy \
-e LANG=en \
-e WG_HOST=<🚨YOUR_SERVER_IP> \
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
-e PORT=51821 \
-e WG_PORT=51820 \
-v ~/.wg-easy:/etc/wireguard \
-v ~/.awg-easy:/etc/amnezia/amneziawg \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--device=/dev/net/tun:/dev/net/tun \
--restart unless-stopped \
ghcr.io/wg-easy/wg-easy:14
ghcr.io/evoll/awg-easy:14
```
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
>
> 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI. See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash.
> 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI.
> See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash.
The Web UI will now be available on `http://0.0.0.0:51821`.
> 💡 Your configuration files will be saved in `~/.wg-easy`
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/)
WireGuard Easy can be launched with Docker Compose as well - just download
[`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
execute `docker compose up --detach`.
### 3. Sponsor
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
> 💡 Your configuration files will be saved in `~/.awg-easy`
## Options
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
| Env | Default | Example | Description |
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. 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. |
| `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_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. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
| `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 |
| Env | Default | Example | Description |
|-------------------------------|-------------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/evoll/awg-easy/blob/v14/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
| `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_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. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/evoll/awg-easy/blob/v14/src/config.js#L19) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/evoll/awg-easy/blob/v14/src/config.js#L20) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/evoll/awg-easy/blob/v14/src/config.js#L27) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/evoll/awg-easy/blob/v14/src/config.js#L28) for the default value. |
| `WG_ENABLE_EXPIRES_TIME` | `false` | `true` | Enable expire time for clients |
| `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 |
| `DICEBEAR_TYPE` | `false` | `bottts` | see [dicebear types](https://www.dicebear.com/styles/) |
| `USE_GRAVATAR` | `false` | `true` | Use or not GRAVATAR service |
| `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/evoll/awg-easy/blob/v14/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
| `JC` | `random` | `5` | Junk packet count — number of packets with random data that are sent before the start of the session. |
| `JMIN` | `50` | `25` | Junk packet minimum size — minimum packet size for Junk packet. That is, all randomly generated packets will have a size no smaller than Jmin. |
| `JMAX` | `1000` | `250` | Junk packet maximum size — maximum size for Junk packets. |
| `S1` | `random` | `75` | Init packet junk size — the size of random data that will be added to the init packet, the size of which is initially fixed. |
| `S2` | `random` | `75` | Response packet junk size — the size of random data that will be added to the response packet, the size of which is initially fixed. |
| `H1` | `random` | `1234567891` | Init packet magic header — the header of the first byte of the handshake. Must be < uint_max. |
| `H2` | `random` | `1234567892` | Response packet magic header — header of the first byte of the handshake response. Must be < uint_max. |
| `H3` | `random` | `1234567893` | Underload packet magic header — UnderLoad packet header. Must be < uint_max. |
| `H4` | `random` | `1234567894` | Transport packet magic header — header of the packet of the data packet. Must be < uint_max. |
> If you change `WG_PORT`, make sure to also change the exposed port.
@ -121,17 +126,14 @@ These options can be configured by setting environment variables using `-e KEY="
To update to the latest version, simply run:
```bash
docker stop wg-easy
docker rm wg-easy
docker pull ghcr.io/wg-easy/wg-easy:14
docker stop awg-easy
docker rm awg-easy
docker pull ghcr.io/evoll/awg-easy
```
And then run the `docker run -d \ ...` command above again.
With Docker Compose WireGuard Easy can be updated with a single command:
`docker compose up --detach --pull always` (if an image tag is specified in the
Compose file and it is not `latest`, make sure that it is changed to the desired
one; by default it is omitted and
[defaults to `latest`](https://docs.docker.com/engine/reference/run/#image-references)). \
The WireGuard Easy container will be automatically recreated if a newer image
was pulled.
## Thanks
Based on [wg-easy](https://github.com/wg-easy/wg-easy) by Emile Nijssen.
Use integrations and adaptation with AmneziaWg from [amnezia-wg-easy](https://github.com/w0rng/amnezia-wg-easy) by w0rng.

BIN
assets/awg-easy.sketch

Binary file not shown.

BIN
assets/screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 182 KiB

BIN
assets/wg-easy.sketch

Binary file not shown.

4
wg-easy.service → awg-easy.service

@ -1,9 +1,9 @@
[Unit]
Description=Wireguard VPN + Web-based Admin UI
Description=AmneziaWG VPN + Web-based Admin UI
After=network-online.target nss-lookup.target
[Service]
Environment="WG_HOST=raspberrypi.local" # Change this to your host's public address or static public ip.
Environment="WG_HOST=local" # Change this to your host's public address or static public ip.
Environment="PASSWORD=REPLACEME" # When set, requires a password when logging in to the Web UI, to disable add a hashtag
#Environment="WG_DEFAULT_ADDRESS=10.0.8.x" #Clients IP address range.
#Environment="WG_DEFAULT_DNS=10.0.8.1, 1.1.1.1" #DNS server clients will use. If set to blank value, clients will not use any DNS.

101
contributing.md

@ -1,101 +0,0 @@
# Contributing to wg-easy
First and foremost, thank you! We appreciate that you want to contribute to wg-easy, your time is valuable, and your contributions mean a lot to us.
## Important!
By contributing to this project, you:
* Agree that you have authored 100% of the content
* Agree that you have the necessary rights to the content
* Agree that you have received the necessary permissions from your employer to make the contributions (if applicable)
* Agree that the content you contribute may be provided under the Project license(s)
* Agree that, if you did not author 100% of the content, the appropriate licenses and copyrights have been added along with any other necessary attribution.
## Getting started
**What does "contributing" mean?**
Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following:
- Updating or correcting documentation
- Feature requests
- Bug reports
## Showing support for wg-easy
Please keep in mind that open source software is built by people like you, who spend their free time creating things the rest the community can use.
Don't have time to contribute? No worries, here are some other ways to show your support for wg-easy:
- star the [project](https://github.com/wg-easy/wg-easy)
- tweet your support for wg-easy
## Issues
Please only create issues for bug reports or feature requests. Issues discussing any other topics may be closed by the project's maintainers without further explanation.
Do not create issues about bumping dependencies unless a bug has been identified and you can demonstrate that it effects this library.
**Help us to help you**
Remember that we’re here to help, but not to make guesses about what you need help with:
- Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you.
- Spell it out completely. Keep in mind that maintainers need to think about _all potential use cases_ of a library. It's important that you explain how you're using a library so that maintainers can make that connection and solve the issue.
_It can't be understated how frustrating and draining it can be to maintainers to have to ask clarifying questions on the most basic things, before it's even possible to start debugging. Please try to make the best use of everyone's time involved, including yourself, by providing this information up front._
### Before creating an issue
Please try to determine if the issue is caused by an underlying library, and if so, create the issue there. Sometimes this is difficult to know. We only ask that you attempt to give a reasonable attempt to find out. Oftentimes the readme will have advice about where to go to create issues.
Try to follow these guidelines:
- **Avoid creating issues for implementation help** - It's much better for discoverability, SEO, and semantics - to keep the issue tracker focused on bugs and feature requests - to ask implementation-related questions on [stackoverflow.com][so]
- **Investigate the issue** - Search for exising issues (open or closed) that address the issue, and might have even resolved it already.
- **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue.
- Create the issue in the appropriate repository.
### Creating an issue
Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue:
- **description**: (required) What is the bug you're experiencing? How are you using this library/app?
- **OS**: (required) what operating system are you on?
- **version**: (required) please note the version of wg-easy are you using
- **error messages**: (required) please paste any error messages into the issue, or a [gist](https://gist.github.com/)
- **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using
### Closing issues
The original poster or the maintainers of wg-easy may close an issue at any time. Typically, but not exclusively, issues are closed when:
- The issue is resolved
- The project's maintainers have determined the issue is out of scope
- An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked.
- A discussion has clearly run its course
## Next steps
**Tips for creating idiomatic issues**
Spending just a little extra time to review best practices and brush up on your contributing skills will, at minimum, make your issue easier to read, easier to resolve, and more likely to be found by others who have the same or similar issue in the future. At best, it will open up doors and potential career opportunities by helping you be at your best.
The following resources were hand-picked to help you be the most effective contributor you can be:
- The [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) is a great place for newcomers to start, but there is also information for experienced contributors there.
- Take some time to learn basic markdown. We can't stress this enough. Don't start pasting code into GitHub issues before you've taken a moment to review this [markdown cheatsheet](https://gist.github.com/jonschlinkert/5854601)
- The GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/) is another great markdown resource.
- Learn about [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). And if you want to really go above and beyond, read [mastering markdown](https://guides.github.com/features/mastering-markdown/).
At the very least, please try to:
- Use backticks to wrap code. This ensures that it retains its formatting and isn't modified when it's rendered by GitHub, and makes the code more readable to others
- When applicable, use syntax highlighting by adding the correct language name after the first "code fence"

4
docker-compose.dev.yml

@ -1,11 +1,11 @@
services:
wg-easy:
awg-easy:
build:
dockerfile: ./Dockerfile
command: npm run serve
volumes:
- ./src/:/app/
# - ./data/:/etc/wireguard
# - ./data/:/etc/amnezia/amneziawg
ports:
- "51820:51820/udp"
- "51821:51821/tcp"

43
docker-compose.yml

@ -1,40 +1,17 @@
volumes:
etc_wireguard:
etc_amnezia:
services:
wg-easy:
environment:
# Change Language:
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi)
- LANG=de
# ⚠️ Required:
# Change this to your host's public address
- WG_HOST=raspberrypi.local
# Optional:
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG # (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
# - PORT=51821
# - WG_PORT=51820
# - WG_CONFIG_PORT=92820
# - WG_DEFAULT_ADDRESS=10.8.0.x
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_MTU=1420
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - WG_PERSISTENT_KEEPALIVE=25
# - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt
# - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
# - 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)
image: ghcr.io/wg-easy/wg-easy:14
container_name: wg-easy
awg-easy:
env_file:
- .env
image: ghcr.io/evoll/awg-easy:14
container_name: awg-easy
volumes:
- etc_wireguard:/etc/wireguard
- etc_amnezia:/etc/amnezia/amneziawg
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
- "${WG_PORT}:${WG_PORT}/udp"
- "${PORT}:${PORT}/tcp"
restart: unless-stopped
cap_add:
- NET_ADMIN
@ -43,3 +20,5 @@ services:
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
devices:
- /dev/net/tun:/dev/net/tun

6
package-lock.json

@ -1,11 +1,11 @@
{
"name": "wg-easy",
"version": "1.0.1",
"name": "awg-easy",
"version": "14.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"version": "1.0.1"
"version": "14.0.0"
}
}
}

12
package.json

@ -1,10 +1,10 @@
{
"version": "1.0.1",
"version": "14.0.0",
"scripts": {
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .",
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag awg-easy .",
"build": "DOCKER_BUILDKIT=1 docker build --tag awg-easy .",
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
"sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy",
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
"sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name awg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/amnezia/amneziawg -p 51820:51820/udp -p 51821:51821/tcp awg-easy",
"start": "docker run --env WG_HOST=0.0.0.0 --name awg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/amnezia/amneziawg -p 51820:51820/udp -p 51821:51821/tcp awg-easy"
}
}
}

27
src/config.js

@ -5,10 +5,9 @@ const { release: { version } } = require('./package.json');
module.exports.RELEASE = version;
module.exports.PORT = process.env.PORT || '51821';
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
/** This is only kept for migration purpose. DO NOT USE! */
module.exports.PASSWORD = process.env.PASSWORD;
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
module.exports.MAX_AGE = parseInt(process.env.MAX_AGE, 10) * 1000 * 60 || 0;
module.exports.WG_PATH = process.env.WG_PATH || '/etc/amnezia/amneziawg/';
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
module.exports.WG_HOST = process.env.WG_HOST;
module.exports.WG_PORT = process.env.WG_PORT || '51820';
@ -39,3 +38,25 @@ 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.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;
module.exports.DICEBEAR_TYPE = process.env.DICEBEAR_TYPE || false;
module.exports.USE_GRAVATAR = process.env.USE_GRAVATAR || false;
const getRandomInt = (min, max) => min + Math.floor(Math.random() * (max - min));
const getRandomJunkSize = () => getRandomInt(15, 150);
const getRandomHeader = () => getRandomInt(1, 2_147_483_647);
module.exports.JC = process.env.JC || getRandomInt(3, 10);
module.exports.JMIN = process.env.JMIN || 50;
module.exports.JMAX = process.env.JMAX || 1000;
module.exports.S1 = process.env.S1 || getRandomJunkSize();
module.exports.S2 = process.env.S2 || getRandomJunkSize();
module.exports.H1 = process.env.H1 || getRandomHeader();
module.exports.H2 = process.env.H2 || getRandomHeader();
module.exports.H3 = process.env.H3 || getRandomHeader();
module.exports.H4 = process.env.H4 || getRandomHeader();

157
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');
@ -28,14 +29,22 @@ const {
PORT,
WEBUI_HOST,
RELEASE,
PASSWORD,
PASSWORD_HASH,
MAX_AGE,
LANG,
UI_TRAFFIC_STATS,
UI_CHART_TYPE,
WG_ENABLE_ONE_TIME_LINKS,
UI_ENABLE_SORT_CLIENTS,
WG_ENABLE_EXPIRES_TIME,
ENABLE_PROMETHEUS_METRICS,
PROMETHEUS_METRICS_PASSWORD,
DICEBEAR_TYPE,
USE_GRAVATAR,
} = require('../config');
const requiresPassword = !!PASSWORD_HASH;
const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD;
/**
* Checks if `password` matches the PASSWORD_HASH.
@ -45,18 +54,22 @@ 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;
};
const cronJobEveryMinute = async () => {
await WireGuard.cronJobEveryMinute();
setTimeout(cronJobEveryMinute, 60 * 1000);
};
module.exports = class Server {
constructor() {
@ -83,9 +96,14 @@ module.exports = class Server {
return `"${LANG}"`;
}))
.get('/api/remember-me', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return MAX_AGE > 0;
}))
.get('/api/ui-traffic-stats', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return `"${UI_TRAFFIC_STATS}"`;
return `${UI_TRAFFIC_STATS}`;
}))
.get('/api/ui-chart-type', defineEventHandler((event) => {
@ -93,6 +111,29 @@ module.exports = class Server {
return `"${UI_CHART_TYPE}"`;
}))
.get('/api/wg-enable-one-time-links', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return `${WG_ENABLE_ONE_TIME_LINKS}`;
}))
.get('/api/ui-sort-clients', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return `${UI_ENABLE_SORT_CLIENTS}`;
}))
.get('/api/wg-enable-expire-time', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return `${WG_ENABLE_EXPIRES_TIME}`;
}))
.get('/api/ui-avatar-settings', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return {
dicebear: DICEBEAR_TYPE,
gravatar: USE_GRAVATAR,
};
}))
// Authentication
.get('/api/session', defineEventHandler((event) => {
const authenticated = requiresPassword
@ -104,8 +145,26 @@ module.exports = class Server {
authenticated,
};
}))
.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.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;
}))
.post('/api/session', defineEventHandler(async (event) => {
const { password } = await readBody(event);
const { password, remember } = await readBody(event);
if (!requiresPassword) {
// if no password is required, the API should never be called.
@ -116,13 +175,16 @@ module.exports = class Server {
});
}
if (!isPasswordValid(password)) {
if (!isPasswordValid(password, PASSWORD_HASH)) {
throw createError({
status: 401,
message: 'Incorrect Password',
});
}
if (MAX_AGE && remember) {
event.node.req.session.cookie.maxAge = MAX_AGE;
}
event.node.req.session.authenticated = true;
event.node.req.session.save();
@ -143,7 +205,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({
@ -193,7 +255,8 @@ module.exports = class Server {
}))
.post('/api/wireguard/client', defineEventHandler(async (event) => {
const { name } = await readBody(event);
await WireGuard.createClient({ name });
const { expiredDate } = await readBody(event);
await WireGuard.createClient({ name, expiredDate });
return { success: true };
}))
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
@ -209,6 +272,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') {
@ -234,6 +311,15 @@ module.exports = class Server {
const { address } = await readBody(event);
await WireGuard.updateClientAddress({ clientId, address });
return { success: true };
}))
.put('/api/wireguard/client/:clientId/expireDate', defineEventHandler(async (event) => {
const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
throw createError({ status: 403 });
}
const { expireDate } = await readBody(event);
await WireGuard.updateClientExpireDate({ clientId, expireDate });
return { success: true };
}));
const safePathJoin = (base, target) => {
@ -259,6 +345,50 @@ module.exports = class Server {
});
};
// Check Prometheus credentials
app.use(
fromNodeMiddleware((req, res, next) => {
if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) {
return next();
}
const user = basicAuth(req);
if (!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 Metrics API
const routerPrometheusMetrics = createRouter();
app.use(routerPrometheusMetrics);
// 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);
@ -297,6 +427,7 @@ module.exports = class Server {
if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json');
if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css');
if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png');
if (id.endsWith('.svg')) setHeader(event, 'Content-Type', 'image/svg+xml');
return {
size: stats.size,
@ -307,12 +438,10 @@ module.exports = class Server {
}),
);
if (PASSWORD) {
throw new Error('DO NOT USE PASSWORD ENVIRONMENT VARIABLE. USE PASSWORD_HASH INSTEAD.\nSee https://github.com/wg-easy/wg-easy/blob/v14/How_to_generate_an_bcrypt_hash.md');
}
createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST);
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
cronJobEveryMinute();
}
};

196
src/lib/WireGuard.js

@ -5,6 +5,7 @@ const path = require('path');
const debug = require('debug')('WireGuard');
const crypto = require('node:crypto');
const QRCode = require('qrcode');
const CRC32 = require('crc-32');
const Util = require('./Util');
const ServerError = require('./ServerError');
@ -23,6 +24,17 @@ const {
WG_POST_UP,
WG_PRE_DOWN,
WG_POST_DOWN,
WG_ENABLE_EXPIRES_TIME,
WG_ENABLE_ONE_TIME_LINKS,
JC,
JMIN,
JMAX,
S1,
S2,
H1,
H2,
H3,
H4,
} = require('../config');
module.exports = class WireGuard {
@ -51,6 +63,15 @@ module.exports = class WireGuard {
privateKey,
publicKey,
address,
jc: JC,
jmin: JMIN,
jmax: JMAX,
s1: S1,
s2: S2,
h1: H1,
h2: H2,
h3: H3,
h4: H4,
},
clients: {},
};
@ -106,6 +127,15 @@ PreUp = ${WG_PRE_UP}
PostUp = ${WG_POST_UP}
PreDown = ${WG_PRE_DOWN}
PostDown = ${WG_POST_DOWN}
Jc = ${config.server.jc}
Jmin = ${config.server.jmin}
Jmax = ${config.server.jmax}
S1 = ${config.server.s1}
S2 = ${config.server.s2}
H1 = ${config.server.h1}
H2 = ${config.server.h2}
H3 = ${config.server.h3}
H4 = ${config.server.h4}
`;
for (const [clientId, client] of Object.entries(config.clients)) {
@ -146,16 +176,22 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
publicKey: client.publicKey,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
expiredAt: client.expiredAt !== null
? new Date(client.expiredAt)
: null,
allowedIPs: client.allowedIPs,
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
const dump = await Util.exec('wg show wg0 dump', {
const dump = await Util.exec('awg show wg0 dump', {
log: false,
});
dump
@ -180,6 +216,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;
@ -208,6 +245,15 @@ PrivateKey = ${client.privateKey ? `${client.privateKey}` : 'REPLACE_ME'}
Address = ${client.address}/24
${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\
${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\
Jc = ${config.server.jc}
Jmin = ${config.server.jmin}
Jmax = ${config.server.jmax}
S1 = ${config.server.s1}
S2 = ${config.server.s2}
H1 = ${config.server.h1}
H2 = ${config.server.h2}
H3 = ${config.server.h3}
H4 = ${config.server.h4}
[Peer]
PublicKey = ${config.server.publicKey}
@ -225,7 +271,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
}
async createClient({ name }) {
async createClient({ name, expiredDate }) {
if (!name) {
throw new Error('Missing: Name');
}
@ -254,7 +300,6 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
if (!address) {
throw new Error('Maximum number of clients reached.');
}
// Create Client
const id = crypto.randomUUID();
const client = {
@ -267,10 +312,15 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
createdAt: new Date(),
updatedAt: new Date(),
expiredAt: null,
enabled: true,
};
if (expiredDate) {
client.expiredAt = new Date(expiredDate);
client.expiredAt.setHours(23);
client.expiredAt.setMinutes(59);
client.expiredAt.setSeconds(59);
}
config.clients[id] = client;
await this.saveConfig();
@ -296,6 +346,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 = new Date(Date.now() + 10 * 1000);
client.updatedAt = new Date();
await this.saveConfig();
}
async disableClient({ clientId }) {
const client = await this.getClient({ clientId });
@ -327,6 +394,22 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await this.saveConfig();
}
async updateClientExpireDate({ clientId, expireDate }) {
const client = await this.getClient({ clientId });
if (expireDate) {
client.expiredAt = new Date(expireDate);
client.expiredAt.setHours(23);
client.expiredAt.setMinutes(59);
client.expiredAt.setSeconds(59);
} else {
client.expiredAt = null;
}
client.updatedAt = new Date();
await this.saveConfig();
}
async __reloadConfig() {
await this.__buildConfig();
await this.__syncConfig();
@ -353,4 +436,107 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await Util.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.expiredAt !== null && new Date() > new Date(client.expiredAt)) {
debug(`Client ${client.id} expired.`);
needSaveConfig = true;
client.enabled = false;
client.updatedAt = new Date();
}
}
}
// 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 awg-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),
};
}
};

202
src/package-lock.json

@ -1,24 +1,27 @@
{
"name": "wg-easy",
"version": "1.0.1",
"name": "awg-easy",
"version": "14.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wg-easy",
"version": "1.0.1",
"name": "awg-easy",
"version": "14.0.0",
"license": "CC BY-NC-SA 4.0",
"dependencies": {
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"debug": "^4.3.6",
"crc-32": "^1.2.2",
"debug": "^4.3.7",
"express-session": "^1.18.0",
"h3": "^1.12.0",
"qrcode": "^1.5.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"eslint-config-athom": "^3.1.3",
"nodemon": "^3.1.4",
"tailwindcss": "^3.4.9"
"tailwindcss": "^3.4.10"
},
"engines": {
"node": ">=18"
@ -451,6 +454,26 @@
"node": ">=14"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
"integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -977,6 +1000,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",
@ -1209,6 +1244,18 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -1306,12 +1353,11 @@
}
},
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"license": "MIT",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "2.1.2"
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@ -1749,9 +1795,9 @@
}
},
"node_modules/eslint-module-utils": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
"integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz",
"integrity": "sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1810,27 +1856,28 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
"integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.3",
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
"array.prototype.findlastindex": "^1.2.5",
"array.prototype.flat": "^1.3.2",
"array.prototype.flatmap": "^1.3.2",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.8.0",
"hasown": "^2.0.0",
"is-core-module": "^2.13.1",
"eslint-module-utils": "^2.9.0",
"hasown": "^2.0.2",
"is-core-module": "^2.15.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.7",
"object.groupby": "^1.0.1",
"object.values": "^1.1.7",
"object.fromentries": "^2.0.8",
"object.groupby": "^1.0.3",
"object.values": "^1.2.0",
"semver": "^6.3.1",
"tsconfig-paths": "^3.15.0"
},
@ -2159,6 +2206,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",
@ -2658,9 +2725,9 @@
}
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
@ -2822,9 +2889,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": {
@ -3223,9 +3290,9 @@
}
},
"node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3248,6 +3315,16 @@
"node": ">=10.0.0"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"dev": true,
"license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -3282,10 +3359,9 @@
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mz": {
"version": "2.7.0",
@ -3704,9 +3780,9 @@
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC"
},
@ -3763,9 +3839,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.45",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
"integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
"dev": true,
"funding": [
{
@ -3905,9 +3981,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4208,23 +4284,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": {
@ -4687,9 +4749,9 @@
"peer": true
},
"node_modules/tailwindcss": {
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5134,9 +5196,9 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"dev": true,
"license": "ISC",
"bin": {

13
src/package.json

@ -2,9 +2,9 @@
"release": {
"version": "14"
},
"name": "wg-easy",
"version": "1.0.1",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"name": "awg-easy",
"version": "14.0.0",
"description": "The easiest way to run AmeziaWG VPN + Web-based Admin UI.",
"main": "server.js",
"scripts": {
"serve": "DEBUG=Server,WireGuard npx nodemon server.js",
@ -15,16 +15,19 @@
"author": "Emile Nijssen",
"license": "CC BY-NC-SA 4.0",
"dependencies": {
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"debug": "^4.3.6",
"crc-32": "^1.2.2",
"debug": "^4.3.7",
"express-session": "^1.18.0",
"h3": "^1.12.0",
"qrcode": "^1.5.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"eslint-config-athom": "^3.1.3",
"nodemon": "^3.1.4",
"tailwindcss": "^3.4.9"
"tailwindcss": "^3.4.10"
},
"nodemonConfig": {
"ignore": [

30
src/wgpw.mjs

@ -2,6 +2,8 @@
// Import needed libraries
import bcrypt from 'bcryptjs';
import { Writable } from 'stream';
import readline from 'readline';
// Function to generate hash
const generateHash = async (password) => {
@ -31,12 +33,35 @@ const comparePassword = async (password, hash) => {
}
};
const readStdinPassword = () => {
return new Promise((resolve) => {
process.stdout.write('Enter your password: ');
const rl = readline.createInterface({
input: process.stdin,
output: new Writable({
write(_chunk, _encoding, callback) {
callback();
},
}),
terminal: true,
});
rl.question('', (answer) => {
rl.close();
// Print a new line after password prompt
process.stdout.write('\n');
resolve(answer);
});
});
};
(async () => {
try {
// Retrieve command line arguments
const args = process.argv.slice(2); // Ignore the first two arguments
if (args.length > 2) {
throw new Error('Usage : wgpw YOUR_PASSWORD [HASH]');
throw new Error('Usage : wgpw [YOUR_PASSWORD] [HASH]');
}
const [password, hash] = args;
@ -44,6 +69,9 @@ const comparePassword = async (password, hash) => {
await comparePassword(password, hash);
} else if (password) {
await generateHash(password);
} else {
const password = await readStdinPassword();
await generateHash(password);
}
} catch (error) {
// eslint-disable-next-line no-console

14
src/www/css/app.css

@ -1,5 +1,5 @@
/*
! tailwindcss v3.4.9 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
*/
/*
@ -714,6 +714,10 @@ video {
margin-bottom: 2.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
@ -1160,6 +1164,10 @@ video {
fill: #4b5563;
}
.p-0 {
padding: 0px;
}
.p-1 {
padding: 0.25rem;
}
@ -1465,6 +1473,10 @@ video {
cursor: default;
}
.p-0 {
padding: 0;
}
.last\:border-b-0:last-child {
border-bottom-width: 0px;
}

BIN
src/www/img/apple-touch-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/www/img/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
src/www/img/favicon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/www/img/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

33
src/www/img/logo.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

109
src/www/index.html

@ -2,11 +2,11 @@
<html>
<head>
<title>WireGuard</title>
<title>AmneziaWG</title>
<meta charset="utf-8"/>
<link href="./css/app.css" rel="stylesheet">
<link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/png" href="./img/favicon.png">
<link rel="icon" href="./img/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="./img/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
@ -27,7 +27,7 @@
<div v-if="authenticated === true">
<div class="flex flex-col-reverse xxs:flex-row flex-auto items-center items-end gap-3">
<h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4">
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg mr-2" /><span class="align-middle">WireGuard</span>
<img src="./img/logo.svg" width="60" class="inline align-middle dark:bg mr-2" /><span class="align-middle">AmneziaWG</span>
</h1>
<div class="flex items-center grow-0 gap-3 items-end self-end xxs:self-center">
<!-- Dark / light theme -->
@ -82,7 +82,7 @@
<p>{{latestRelease.changelog}}</p>
</div>
<a href="https://github.com/wg-easy/wg-easy#updating" target="_blank"
<a href="https://github.com/evoll/awg-easy-14#updating" target="_blank"
class="p-3 rounded-md bg-white dark:bg-red-100 float-right font-sm font-semibold text-red-800 dark:text-red-600 flex-shrink-0 border-2 border-red-800 dark:border-red-600 hover:border-white dark:hover:border-red-600 hover:text-white dark:hover:text-red-100 hover:bg-red-800 dark:hover:bg-red-600 transition-all">
{{$t("update")}} →
</a>
@ -112,8 +112,22 @@
</svg>
<span class="max-md:hidden text-sm">{{$t("backup")}}</span>
</a>
<!-- Sort client -->
<button v-if="enableSortClient" @click="sortClient = !sortClient;"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition">
<svg v-if="sortClient === true" inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path d="M12 19.75C11.9015 19.7504 11.8038 19.7312 11.7128 19.6934C11.6218 19.6557 11.5392 19.6001 11.47 19.53L5.47 13.53C5.33752 13.3878 5.2654 13.1997 5.26882 13.0054C5.27225 12.8111 5.35096 12.6258 5.48838 12.4883C5.62579 12.3509 5.81118 12.2722 6.00548 12.2688C6.19978 12.2654 6.38782 12.3375 6.53 12.47L12 17.94L17.47 12.47C17.6122 12.3375 17.8002 12.2654 17.9945 12.2688C18.1888 12.2722 18.3742 12.3509 18.5116 12.4883C18.649 12.6258 18.7277 12.8111 18.7312 13.0054C18.7346 13.1997 18.6625 13.3878 18.53 13.53L12.53 19.53C12.4608 19.6001 12.3782 19.6557 12.2872 19.6934C12.1962 19.7312 12.0985 19.7504 12 19.75Z" fill="#000000"/>
<path d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z" fill="#000000"/>
</svg>
<svg v-if="sortClient === false" inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path d="M18 11.75C17.9015 11.7505 17.8038 11.7313 17.7128 11.6935C17.6218 11.6557 17.5392 11.6001 17.47 11.53L12 6.06001L6.53 11.53C6.38782 11.6625 6.19978 11.7346 6.00548 11.7312C5.81118 11.7278 5.62579 11.649 5.48838 11.5116C5.35096 11.3742 5.27225 11.1888 5.26882 10.9945C5.2654 10.8002 5.33752 10.6122 5.47 10.47L11.47 4.47001C11.6106 4.32956 11.8012 4.25067 12 4.25067C12.1987 4.25067 12.3894 4.32956 12.53 4.47001L18.53 10.47C18.6705 10.6106 18.7493 10.8013 18.7493 11C18.7493 11.1988 18.6705 11.3894 18.53 11.53C18.4608 11.6001 18.3782 11.6557 18.2872 11.6935C18.1962 11.7313 18.0985 11.7505 18 11.75Z" fill="#000000"/>
<path d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z" fill="#000000"/>
</svg>
<span class="max-md:hidden text-sm">{{$t("sort")}}</span>
</button>
<!-- New client -->
<button @click="clientCreate = true; clientCreateName = '';"
<button @click="clientCreate = true; clientCreateName = ''; clientExpiredDate = '';"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-r-full md:rounded inline-flex items-center transition">
<svg class="w-4 md:mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
@ -242,6 +256,35 @@
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
</span>
</div>
<div v-if="enableOneTimeLinks && client.oneTimeLink !== null && client.oneTimeLink !== ''" :ref="'client-' + client.id + '-link'" class="text-gray-400 text-xs">
<a :href="'./cnf/' + client.oneTimeLink + ''">{{document.location.protocol}}//{{document.location.host}}/cnf/{{client.oneTimeLink}}</a>
</div>
<!-- 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">
<span class="group">
<!-- Show -->
<input v-show="clientEditExpireDateId === client.id" v-model="clientEditExpireDate"
v-on:keyup.enter="updateClientExpireDate(client, clientEditExpireDate); clientEditExpireDate = null; clientEditExpireDateId = null;"
v-on:keyup.escape="clientEditExpireDate = null; clientEditExpireDateId = null;"
:ref="'client-' + client.id + '-expire'"
type="text"
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-70 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500 text-xs p-0" />
<span v-show="clientEditExpireDateId !== client.id"
class="inline-block ">{{client.expiredAt | expiredDateFormat}}</span>
<!-- Edit -->
<span v-show="clientEditExpireDateId !== client.id"
@click="clientEditExpireDate = client.expiredAt ? client.expiredAt.toISOString().slice(0, 10) : 'yyyy-mm-dd'; clientEditExpireDateId = client.id; setTimeout(() => $refs['client-' + client.id + '-expire'][0].select(), 1);"
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</span>
</span>
</div>
</div>
<!-- Info -->
@ -341,6 +384,22 @@
</svg>
</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 -->
<button
@ -361,7 +420,7 @@
<div v-if="clients && clients.length === 0">
<p class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm">
{{$t("noClients")}}<br /><br />
<button @click="clientCreate = true; clientCreateName = '';"
<button @click="clientCreate = true; clientCreateName = ''; clientExpiredDate = '';"
class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition">
<svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
@ -453,6 +512,16 @@
type="text" v-model.trim="clientCreateName" :placeholder="$t('name')" />
</p>
</div>
<div class="mt-2" v-show="enableExpireTime">
<p class="text-sm text-gray-500">
<label class="block text-gray-900 dark:text-neutral-200 text-sm font-bold mb-2" for="expireDate">
{{$t("ExpireDate")}}
</label>
<input
class="rounded p-2 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none w-full"
type="date" v-model.trim="clientExpiredDate" :placeholder="$t('expireDate')" name="expireDate"/>
</p>
</div>
</div>
</div>
</div>
@ -546,8 +615,8 @@
<div v-if="authenticated === false">
<h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center">
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg" />
<span class="align-middle">WireGuard</span>
<img src="./img/logo.svg" width="60" class="inline align-middle dark:bg" />
<span class="align-middle">AmneziaWG</span>
</h1>
<form @submit="login"
@ -563,6 +632,24 @@
<input type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
<!-- Remember me -->
<label v-if="rememberMeEnabled"
class="inline-block mb-5 cursor-pointer whitespace-nowrap" :title="$t('titleRememberMe')">
<input type="checkbox" class="sr-only" v-model="remember">
<div v-if="remember"
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all">
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
</div>
<div v-if="!remember"
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 dark:bg-neutral-400 cursor-pointer hover:bg-gray-300 dark:hover:bg-neutral-500 transition-all">
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
</div>
<span class="text-sm">{{$t("rememberMe")}}</span>
</label>
<button v-if="authenticating"
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed">
<svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
@ -596,12 +683,6 @@
</div>
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <a class="hover:underline" target="_blank"
href="https://github.com/wg-easy/wg-easy">WireGuard Easy</a> © 2021-2024 by <a class="hover:underline" target="_blank"
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> is licensed under <a class="hover:underline" target="_blank"
href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> · <a class="hover:underline"
href="https://github.com/sponsors/WeeJeWel" target="_blank">{{$t("donate")}}</a></p>
</div>
<script src="./js/vendor/vue.min.js"></script>

61
src/www/js/api.js

@ -43,6 +43,13 @@ class API {
});
}
async getRememberMeEnabled() {
return this.call({
method: 'get',
path: '/remember-me',
});
}
async getuiTrafficStats() {
return this.call({
method: 'get',
@ -57,6 +64,27 @@ class API {
});
}
async getWGEnableOneTimeLinks() {
return this.call({
method: 'get',
path: '/wg-enable-one-time-links',
});
}
async getWGEnableExpireTime() {
return this.call({
method: 'get',
path: '/wg-enable-expire-time',
});
}
async getAvatarSettings() {
return this.call({
method: 'get',
path: '/ui-avatar-settings',
});
}
async getSession() {
return this.call({
method: 'get',
@ -64,11 +92,11 @@ class API {
});
}
async createSession({ password }) {
async createSession({ password, remember }) {
return this.call({
method: 'post',
path: '/session',
body: { password },
body: { password, remember },
});
}
@ -87,17 +115,20 @@ class API {
...client,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
expiredAt: client.expiredAt !== null
? new Date(client.expiredAt)
: null,
latestHandshakeAt: client.latestHandshakeAt !== null
? new Date(client.latestHandshakeAt)
: null,
})));
}
async createClient({ name }) {
async createClient({ name, expiredDate }) {
return this.call({
method: 'post',
path: '/wireguard/client',
body: { name },
body: { name, expiredDate },
});
}
@ -108,6 +139,13 @@ class API {
});
}
async showOneTimeLink({ clientId }) {
return this.call({
method: 'post',
path: `/wireguard/client/${clientId}/generateOneTimeLink`,
});
}
async enableClient({ clientId }) {
return this.call({
method: 'post',
@ -138,6 +176,14 @@ class API {
});
}
async updateClientExpireDate({ clientId, expireDate }) {
return this.call({
method: 'put',
path: `/wireguard/client/${clientId}/expireDate/`,
body: { expireDate },
});
}
async restoreConfiguration(file) {
return this.call({
method: 'put',
@ -146,4 +192,11 @@ class API {
});
}
async getUiSortClients() {
return this.call({
method: 'get',
path: '/ui-sort-clients',
});
}
}

104
src/www/js/app.js

@ -23,6 +23,22 @@ function bytes(bytes, decimals, kib, maxunit) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
/**
* Sorts an array of objects by a specified property in ascending or descending order.
*
* @param {Array} array - The array of objects to be sorted.
* @param {string} property - The property to sort the array by.
* @param {boolean} [sort=true] - Whether to sort the array in ascending (default) or descending order.
* @return {Array} - The sorted array of objects.
*/
function sortByProperty(array, property, sort = true) {
if (sort) {
return array.sort((a, b) => (typeof a[property] === 'string' ? a[property].localeCompare(b[property]) : a[property] - b[property]));
}
return array.sort((a, b) => (typeof a[property] === 'string' ? b[property].localeCompare(a[property]) : b[property] - a[property]));
}
const i18n = new VueI18n({
locale: localStorage.getItem('lang') || 'en',
fallbackLocale: 'en',
@ -53,16 +69,21 @@ new Vue({
authenticating: false,
password: null,
requiresPassword: null,
remember: false,
rememberMeEnabled: false,
clients: null,
clientsPersist: {},
clientDelete: null,
clientCreate: null,
clientCreateName: '',
clientExpiredDate: '',
clientEditName: null,
clientEditNameId: null,
clientEditAddress: null,
clientEditAddressId: null,
clientEditExpireDate: null,
clientEditExpireDateId: null,
qrcode: null,
currentRelease: null,
@ -71,6 +92,15 @@ new Vue({
uiTrafficStats: false,
uiChartType: 0,
avatarSettings: {
dicebear: null,
gravatar: false,
},
enableOneTimeLinks: false,
enableSortClient: false,
sortClient: true, // Sort clients by name, true = asc, false = desc
enableExpireTime: false,
uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
uiTheme: localStorage.theme || 'auto',
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
@ -155,6 +185,7 @@ new Vue({
},
},
},
},
methods: {
dateTime: (value) => {
@ -173,8 +204,10 @@ new Vue({
const clients = await this.api.getClients();
this.clients = clients.map((client) => {
if (client.name.includes('@') && client.name.includes('.')) {
if (client.name.includes('@') && client.name.includes('.') && this.avatarSettings.gravatar) {
client.avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`;
} else if (this.avatarSettings.dicebear) {
client.avatar = `https://api.dicebear.com/9.x/${this.avatarSettings.dicebear}/svg?seed=${sha256(client.name.toLowerCase().trim())}`;
}
if (!this.clientsPersist[client.id]) {
@ -229,6 +262,10 @@ new Vue({
return client;
});
if (this.enableSortClient) {
this.clients = sortByProperty(this.clients, 'name', this.sortClient);
}
},
login(e) {
e.preventDefault();
@ -239,6 +276,7 @@ new Vue({
this.authenticating = true;
this.api.createSession({
password: this.password,
remember: this.remember,
})
.then(async () => {
const session = await this.api.getSession();
@ -268,9 +306,10 @@ new Vue({
},
createClient() {
const name = this.clientCreateName;
const expiredDate = this.clientExpiredDate;
if (!name) return;
this.api.createClient({ name })
this.api.createClient({ name, expiredDate })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
@ -279,6 +318,11 @@ new Vue({
.catch((err) => alert(err.message || err.toString()))
.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) {
this.api.enableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
@ -299,6 +343,11 @@ new Vue({
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
updateClientExpireDate(client, expireDate) {
this.api.updateClientExpireDate({ clientId: client.id, expireDate })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
restoreConfig(e) {
e.preventDefault();
const file = e.currentTarget.files.item(0);
@ -342,6 +391,15 @@ new Vue({
timeago: (value) => {
return timeago.format(value, i18n.locale);
},
expiredDateFormat: (value) => {
if (value === null) return i18n.t('Permanent');
const dateTime = new Date(value);
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return dateTime.toLocaleDateString(i18n.locale, options);
},
expiredDateEditFormat: (value) => {
if (value === null) return 'yyyy-MM-dd';
},
},
mounted() {
this.prefersDarkScheme.addListener(this.handlePrefersChange);
@ -362,6 +420,11 @@ new Vue({
alert(err.message || err.toString());
});
this.api.getRememberMeEnabled()
.then((rememberMeEnabled) => {
this.rememberMeEnabled = rememberMeEnabled;
});
setInterval(() => {
this.refresh({
updateCharts: this.updateCharts,
@ -384,6 +447,41 @@ new Vue({
this.uiChartType = 0;
});
this.api.getWGEnableOneTimeLinks()
.then((res) => {
this.enableOneTimeLinks = res;
})
.catch(() => {
this.enableOneTimeLinks = false;
});
this.api.getUiSortClients()
.then((res) => {
this.enableSortClient = res;
})
.catch(() => {
this.enableSortClient = false;
});
this.api.getWGEnableExpireTime()
.then((res) => {
this.enableExpireTime = res;
})
.catch(() => {
this.enableExpireTime = false;
});
this.api.getAvatarSettings()
.then((res) => {
this.avatarSettings = res;
})
.catch(() => {
this.avatarSettings = {
dicebear: null,
gravatar: false,
};
});
Promise.resolve().then(async () => {
const lang = await this.api.getLang();
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
@ -392,7 +490,7 @@ new Vue({
}
const currentRelease = await this.api.getRelease();
const latestRelease = await fetch('https://wg-easy.github.io/wg-easy/changelog.json')
const latestRelease = await fetch('https://evoll.github.io/awg-easy-14/changelog.json')
.then((res) => res.json())
.then((releases) => {
const releasesArray = Object.entries(releases).map(([version, changelog]) => ({

53
src/www/js/i18n.js

@ -34,6 +34,12 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Backup',
titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration',
rememberMe: 'Remember me',
titleRememberMe: 'Stay logged after closing the browser',
sort: 'Sort',
ExpireDate: 'Expire Date',
Permanent: 'Permanent',
OneTimeLink: 'Generate short one time link',
},
ua: {
name: 'Ім`я',
@ -102,6 +108,12 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Резервная копия',
titleRestoreConfig: 'Восстановить конфигурацию',
titleBackupConfig: 'Создать резервную копию конфигурации',
rememberMe: 'Запомнить меня',
titleRememberMe: 'Оставаться в системе после закрытия браузера',
sort: 'Сортировка',
ExpireDate: 'Дата истечения срока',
Permanent: 'Бессрочно',
OneTimeLink: 'Создать короткую одноразовую ссылку',
},
tr: { // Müslüm Barış Korkmazer @babico
name: 'İsim',
@ -340,8 +352,14 @@ const messages = { // eslint-disable-line no-unused-vars
downloadConfig: '구성 다운로드',
madeBy: '만든 사람',
donate: '기부',
toggleCharts: '차트 표시/숨기기',
theme: { dark: '어두운 테마', light: '밝은 테마', auto: '자동 테마' },
restore: '복원',
backup: '백업',
titleRestoreConfig: '구성 파일 복원',
titleBackupConfig: '구성 파일 백업',
},
vi: {
vi: { // https://github.com/hoangneeee
name: 'Tên',
password: 'Mật khẩu',
signIn: 'Đăng nhập',
@ -367,6 +385,13 @@ const messages = { // eslint-disable-line no-unused-vars
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',
@ -471,10 +496,23 @@ const messages = { // eslint-disable-line no-unused-vars
disableClient: '禁用客户端',
enableClient: '启用客户端',
noClients: '目前没有客户端。',
noPrivKey: '此客户端没有已知的私钥。无法创建配置。',
showQR: '显示二维码',
downloadConfig: '下载配置',
madeBy: '由',
donate: '捐赠',
toggleCharts: '显示/隐藏图表',
theme: { dark: '暗黑主题', light: '明亮主题', auto: '自动主题' },
restore: '恢复',
backup: '备份',
titleRestoreConfig: '恢复您的配置',
titleBackupConfig: '备份您的配置',
rememberMe: '记住我',
titleRememberMe: '关闭浏览器后保持登录',
sort: '排序',
ExpireDate: '到期日期',
Permanent: '永久',
OneTimeLink: '生成一次性短链接',
},
cht: {
name: '名字',
@ -498,10 +536,23 @@ const messages = { // eslint-disable-line no-unused-vars
disableClient: '停用使用者',
enableClient: '啟用使用者',
noClients: '目前沒有使用者。',
noPrivKey: '此使用者沒有已知的私鑰。無法創建配置。',
showQR: '顯示 QR Code',
downloadConfig: '下載 Config 檔',
madeBy: '由',
donate: '抖內',
toggleCharts: '顯示/隱藏圖表',
theme: { dark: '暗黑主題', light: '明亮主題', auto: '自動主題' },
restore: '恢復',
backup: '備份',
titleRestoreConfig: '恢復您的配置',
titleBackupConfig: '備份您的配置',
rememberMe: '記住我',
titleRememberMe: '關閉瀏覽器後保持登錄',
sort: '排序',
ExpireDate: '到期日期',
Permanent: '永久',
OneTimeLink: '生成一次性短鏈接',
},
it: {
name: 'Nome',

6
src/www/manifest.json

@ -1,11 +1,11 @@
{
"name": "WireGuard",
"name": "AmneziaWG",
"display": "standalone",
"background_color": "#fff",
"icons": [
{
"src": "img/favicon.png",
"type": "image/png"
"src": "img/favicon.ico",
"type": "image/x-icon"
}
]
}

4
src/www/src/css/app.css

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.p-0 {
padding: 0;
}

Loading…
Cancel
Save