├── .dockerignore
├── .gitignore
├── src
├── www
│ ├── src
│ │ └── css
│ │ │ └── app.css
│ ├── img
│ │ ├── favicon.ico
│ │ ├── apple-touch-icon.png
│ │ └── logo.svg
│ ├── manifest.json
│ └── js
│ │ ├── api.js
│ │ ├── vendor
│ │ ├── vue-apexcharts.min.js
│ │ ├── sha256.min.js
│ │ ├── timeago.full.min.js
│ │ └── vue-i18n.min.js
│ │ ├── app.js
│ │ └── i18n.js
├── services
│ ├── Server.js
│ └── WireGuard.js
├── .eslintrc.json
├── lib
│ ├── ServerError.js
│ ├── Util.js
│ ├── Server.js
│ └── WireGuard.js
├── tailwind.config.js
├── server.js
├── package.json
└── config.js
├── assets
└── screenshot.png
├── .github
├── CODEOWNERS
├── dependabot.yml
├── workflows
│ ├── lint.yml
│ ├── codeql.yml
│ ├── deploy-development.yml
│ ├── npm-update-bot.yml
│ └── deploy.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── docs
└── changelog.json
├── .env
├── package.json
├── docker-compose.yml
├── Dockerfile
├── contributing.md
├── README.md
└── LICENSE
/.dockerignore:
--------------------------------------------------------------------------------
1 | /src/node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /config
2 | /wg0.conf
3 | /wg0.json
4 | /src/node_modules
5 | .DS_Store
6 | *.swp
7 |
--------------------------------------------------------------------------------
/src/www/src/css/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spcfox/amnezia-wg-easy/HEAD/assets/screenshot.png
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Copyright (c) Emile Nijssen
2 | # Founder and Codeowner of WireGuard Easy (wg-easy)
3 |
--------------------------------------------------------------------------------
/src/www/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spcfox/amnezia-wg-easy/HEAD/src/www/img/favicon.ico
--------------------------------------------------------------------------------
/src/www/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spcfox/amnezia-wg-easy/HEAD/src/www/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/services/Server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Server = require('../lib/Server');
4 |
5 | module.exports = new Server();
6 |
--------------------------------------------------------------------------------
/src/services/WireGuard.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const WireGuard = require('../lib/WireGuard');
4 |
5 | module.exports = new WireGuard();
6 |
--------------------------------------------------------------------------------
/docs/changelog.json:
--------------------------------------------------------------------------------
1 | {
2 | "1": "Initial version. Enjoy!",
3 | "2": "UI_TRAFFIC_STATS, UI_CHART_TYPE, other upgrades from wg-easy and bugfixes."
4 | }
5 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | rebase-strategy: "auto"
8 |
--------------------------------------------------------------------------------
/src/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "athom",
3 | "ignorePatterns": [
4 | "**/vendor/*.js"
5 | ],
6 | "rules": {
7 | "consistent-return": "off",
8 | "no-shadow": "off",
9 | "max-len": "off"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/ServerError.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = class ServerError extends Error {
4 |
5 | constructor(message, statusCode = 500) {
6 | super(message);
7 | this.statusCode = statusCode;
8 | }
9 |
10 | };
11 |
--------------------------------------------------------------------------------
/src/www/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AmneziaWG",
3 | "display": "standalone",
4 | "background_color": "#fff",
5 | "icons": [
6 | {
7 | "src": "img/favicon.ico",
8 | "type": "image/x-icon"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | WG_HOST=🚨YOUR_SERVER_IP
2 | PASSWORD=🚨YOUR_ADMIN_PASSWORD
3 | # (Supports: en, ru, tr, no, pl, fr, de, ca, es)
4 | LANGUAGE=en
5 | PORT=51821
6 | WG_DEVICE=eth0
7 | WG_PORT=51820
8 | WG_DEFAULT_ADDRESS=10.8.0.x
9 | WG_DEFAULT_DNS=1.1.1.1
10 | WG_ALLOWED_IPS=0.0.0.0/0, ::/0
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "scripts": {
4 | "build": "DOCKER_BUILDKIT=1 docker build --tag amnezia-wg-easy .",
5 | "start": "docker run --env WG_HOST=0.0.0.0 --name amnezia-wg-easy --device=/dev/net/tun:/dev/net/tun --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 amnezia-wg-easy"
6 | }
7 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | volumes:
2 | etc_amneziawg:
3 |
4 | services:
5 | amnezia-wg-easy:
6 | env_file:
7 | - .env
8 | image: ghcr.io/spcfox/amnezia-wg-easy
9 | container_name: amnezia-wg-easy
10 | volumes:
11 | - ~/.amnezia-wg-easy:/etc/wireguard
12 | ports:
13 | - "${WG_PORT}:51820/udp"
14 | - "${PORT}:${PORT}/tcp"
15 | restart: unless-stopped
16 | cap_add:
17 | - NET_ADMIN
18 | - SYS_MODULE
19 | sysctls:
20 | - net.ipv4.ip_forward=1
21 | - net.ipv4.conf.all.src_valid_mark=1
22 | devices:
23 | - /dev/net/tun:/dev/net/tun
24 |
--------------------------------------------------------------------------------
/src/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | 'use strict';
4 |
5 | module.exports = {
6 | darkMode: 'selector',
7 | content: ['./www/**/*.{html,js}'],
8 | theme: {
9 | screens: {
10 | xxs: '450px',
11 | xs: '576px',
12 | sm: '640px',
13 | md: '768px',
14 | lg: '1024px',
15 | xl: '1280px',
16 | '2xl': '1536px',
17 | },
18 | },
19 | plugins: [
20 | function addDisabledClass({ addUtilities }) {
21 | const newUtilities = {
22 | '.is-disabled': {
23 | opacity: '0.25',
24 | cursor: 'default',
25 | },
26 | };
27 | addUtilities(newUtilities);
28 | },
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - production
8 | pull_request:
9 |
10 | jobs:
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v4
17 | - name: Setup Node
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: '20'
21 | check-latest: true
22 | cache: 'npm'
23 | cache-dependency-path: |
24 | package-lock.json
25 | src/package-lock.json
26 |
27 | - name: npm run lint
28 | run: |
29 | cd src
30 | npm ci
31 | npm run lint
32 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./services/Server');
4 |
5 | const WireGuard = require('./services/WireGuard');
6 |
7 | WireGuard.getConfig()
8 | .catch((err) => {
9 | // eslint-disable-next-line no-console
10 | console.error(err);
11 |
12 | // eslint-disable-next-line no-process-exit
13 | process.exit(1);
14 | });
15 |
16 | // Handle terminate signal
17 | process.on('SIGTERM', async () => {
18 | // eslint-disable-next-line no-console
19 | console.log('SIGTERM signal received.');
20 | await WireGuard.Shutdown();
21 | // eslint-disable-next-line no-process-exit
22 | process.exit(0);
23 | });
24 |
25 | // Handle interrupt signal
26 | process.on('SIGINT', () => {
27 | // eslint-disable-next-line no-console
28 | console.log('SIGINT signal received.');
29 | });
30 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "release": "2",
3 | "name": "amnezia-wg-easy",
4 | "version": "1.0.0",
5 | "description": "The easiest way to run AmneziaWG VPN + Web-based Admin UI.",
6 | "main": "server.js",
7 | "scripts": {
8 | "serve": "DEBUG=Server,WireGuard npx nodemon server.js",
9 | "serve-with-password": "PASSWORD=wg npm run serve",
10 | "lint": "eslint .",
11 | "buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
12 | },
13 | "author": "Viktor Yudov",
14 | "license": "GPL",
15 | "dependencies": {
16 | "debug": "^4.3.6",
17 | "express-session": "^1.18.0",
18 | "h3": "^1.12.0",
19 | "qrcode": "^1.5.4"
20 | },
21 | "devDependencies": {
22 | "eslint-config-athom": "^3.1.3",
23 | "nodemon": "^3.1.4",
24 | "tailwindcss": "^3.4.9"
25 | },
26 | "nodemonConfig": {
27 | "ignore": [
28 | "www/*"
29 | ]
30 | },
31 | "engines": {
32 | "node": ">=18"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 | schedule:
9 | - cron: "15 0 * * *"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ 'javascript-typescript' ]
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v4
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v3
31 | with:
32 | languages: ${{ matrix.language }}
33 |
34 | - name: Autobuild
35 | uses: github/codeql-action/autobuild@v3
36 |
37 | - name: Perform CodeQL Analysis
38 | uses: github/codeql-action/analyze@v3
39 | with:
40 | category: "/language:${{matrix.language}}"
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. macOS 12.1]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS 8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-development.yml:
--------------------------------------------------------------------------------
1 | name: Build & Publish Development
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | deploy:
8 | name: Build & Deploy
9 | runs-on: ubuntu-latest
10 | if: github.repository_owner == 'spcfox'
11 | permissions:
12 | packages: write
13 | contents: read
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | ref: master
18 |
19 | - name: Set up QEMU
20 | uses: docker/setup-qemu-action@v3
21 |
22 | - name: Set up Docker Buildx
23 | uses: docker/setup-buildx-action@v3
24 |
25 | - name: Login to GitHub Container Registry
26 | uses: docker/login-action@v3
27 | with:
28 | registry: ghcr.io
29 | username: ${{ github.actor }}
30 | password: ${{ secrets.GITHUB_TOKEN }}
31 |
32 | - name: Build & Publish Docker Image
33 | uses: docker/build-push-action@v6
34 | with:
35 | push: true
36 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
37 | tags: ghcr.io/spcfox/amnezia-wg-easy:development
38 |
--------------------------------------------------------------------------------
/.github/workflows/npm-update-bot.yml:
--------------------------------------------------------------------------------
1 | name: NPM Update Bot 🤖
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | # schedule:
7 | # - cron: "0 0 * * 1"
8 |
9 | jobs:
10 | npmupbot:
11 | name: NPM Update Bot 🤖
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v4
16 | with:
17 | repository: spcfox/amnezia-wg-easy
18 | ref: master
19 | - name: Setup Node
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: '20'
23 | check-latest: true
24 | cache: 'npm'
25 | cache-dependency-path: |
26 | package-lock.json
27 | src/package-lock.json
28 |
29 | - name: Bot 🤖 "Updating NPM Packages..."
30 | run: |
31 | npm install -g --silent npm-check-updates
32 | ncu -u
33 | npm update
34 | cd src
35 | ncu -u
36 | npm update
37 | npm run buildcss
38 | git config --global user.name 'NPM Update Bot'
39 | git config --global user.email 'npmupbot@users.noreply.github.com'
40 | git add .
41 | git commit -am "npm: package updates" || true
42 | git push
43 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build & Publish Latest
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - production
8 |
9 | jobs:
10 | deploy:
11 | name: Build & Deploy
12 | runs-on: ubuntu-latest
13 | if: github.repository_owner == 'spcfox'
14 | permissions:
15 | packages: write
16 | contents: read
17 | steps:
18 | - uses: actions/checkout@v4
19 | with:
20 | ref: production
21 |
22 | - name: Set up QEMU
23 | uses: docker/setup-qemu-action@v3
24 |
25 | - name: Set up Docker Buildx
26 | uses: docker/setup-buildx-action@v3
27 |
28 | - name: Login to GitHub Container Registry
29 | uses: docker/login-action@v3
30 | with:
31 | registry: ghcr.io
32 | username: ${{ github.actor }}
33 | password: ${{ secrets.GITHUB_TOKEN }}
34 |
35 | - name: Set environment variables
36 | run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV
37 |
38 | - name: Build & Publish Docker Image
39 | uses: docker/build-push-action@v6
40 | with:
41 | push: true
42 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
43 | tags: ghcr.io/spcfox/amnezia-wg-easy:latest, ghcr.io/spcfox/amnezia-wg-easy:${{ env.RELEASE }}
44 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # As a workaround we have to build on nodejs 18
2 | # nodejs 20 hangs on build with armv6/armv7
3 | FROM docker.io/library/node:18-alpine AS build_node_modules
4 |
5 | # Update npm to latest
6 | RUN npm install -g npm@latest
7 |
8 | # Copy Web UI
9 | COPY src /app
10 | WORKDIR /app
11 | RUN npm ci --omit=dev &&\
12 | mv node_modules /node_modules
13 |
14 | # Copy build result to a new image.
15 | # This saves a lot of disk space.
16 | FROM amneziavpn/amnezia-wg:latest
17 | 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
18 | COPY --from=build_node_modules /app /app
19 |
20 | # Install Node.js
21 | RUN apk add --no-cache \
22 | nodejs \
23 | npm
24 |
25 | # Move node_modules one directory up, so during development
26 | # we don't have to mount it in a volume.
27 | # This results in much faster reloading!
28 | #
29 | # Also, some node_modules might be native, and
30 | # the architecture & OS of your development machine might differ
31 | # than what runs inside of docker.
32 | COPY --from=build_node_modules /node_modules /node_modules
33 |
34 | # Install Linux packages
35 | RUN apk add --no-cache \
36 | dpkg \
37 | dumb-init \
38 | iptables
39 |
40 | # Use iptables-legacy
41 | 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
42 |
43 | # Set Environment
44 | ENV DEBUG=Server,WireGuard
45 |
46 | # Run Web UI
47 | WORKDIR /app
48 | CMD ["/usr/bin/dumb-init", "node", "server.js"]
49 |
--------------------------------------------------------------------------------
/src/lib/Util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const childProcess = require('child_process');
4 |
5 | module.exports = class Util {
6 |
7 | static isValidIPv4(str) {
8 | const blocks = str.split('.');
9 | if (blocks.length !== 4) return false;
10 |
11 | for (let value of blocks) {
12 | value = parseInt(value, 10);
13 | if (Number.isNaN(value)) return false;
14 | if (value < 0 || value > 255) return false;
15 | }
16 |
17 | return true;
18 | }
19 |
20 | static promisify(fn) {
21 | // eslint-disable-next-line func-names
22 | return function(req, res) {
23 | Promise.resolve().then(async () => fn(req, res))
24 | .then((result) => {
25 | if (res.headersSent) return;
26 |
27 | if (typeof result === 'undefined') {
28 | return res
29 | .status(204)
30 | .end();
31 | }
32 |
33 | return res
34 | .status(200)
35 | .json(result);
36 | })
37 | .catch((error) => {
38 | if (typeof error === 'string') {
39 | error = new Error(error);
40 | }
41 |
42 | // eslint-disable-next-line no-console
43 | console.error(error);
44 |
45 | return res
46 | .status(error.statusCode || 500)
47 | .json({
48 | error: error.message || error.toString(),
49 | stack: error.stack,
50 | });
51 | });
52 | };
53 | }
54 |
55 | static async exec(cmd, {
56 | log = true,
57 | } = {}) {
58 | if (typeof log === 'string') {
59 | // eslint-disable-next-line no-console
60 | console.log(`$ ${log}`);
61 | } else if (log === true) {
62 | // eslint-disable-next-line no-console
63 | console.log(`$ ${cmd}`);
64 | }
65 |
66 | if (process.platform !== 'linux') {
67 | return '';
68 | }
69 |
70 | return new Promise((resolve, reject) => {
71 | childProcess.exec(cmd, {
72 | shell: 'bash',
73 | }, (err, stdout) => {
74 | if (err) return reject(err);
75 | return resolve(String(stdout).trim());
76 | });
77 | });
78 | }
79 |
80 | };
81 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { release } = require('./package.json');
4 |
5 | module.exports.CHECK_UPDATE = process.env.CHECK_UPDATE ? process.env.CHECK_UPDATE.toLowerCase() === 'true' : true;
6 | module.exports.RELEASE = release;
7 | module.exports.PORT = process.env.PORT || '51821';
8 | module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
9 | module.exports.PASSWORD = process.env.PASSWORD;
10 | module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
11 | module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
12 | module.exports.WG_HOST = process.env.WG_HOST;
13 | module.exports.WG_PORT = process.env.WG_PORT || '51820';
14 | module.exports.WG_MTU = process.env.WG_MTU || null;
15 | module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0';
16 | module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x';
17 | module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string'
18 | ? process.env.WG_DEFAULT_DNS
19 | : '1.1.1.1';
20 | module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0';
21 |
22 | module.exports.WG_PRE_UP = process.env.WG_PRE_UP || '';
23 | module.exports.WG_POST_UP = process.env.WG_POST_UP || `
24 | iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE;
25 | iptables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT;
26 | iptables -A FORWARD -i wg0 -j ACCEPT;
27 | iptables -A FORWARD -o wg0 -j ACCEPT;
28 | `.split('\n').join(' ');
29 |
30 | module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || '';
31 | module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || `
32 | iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE;
33 | iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT;
34 | iptables -D FORWARD -i wg0 -j ACCEPT;
35 | iptables -D FORWARD -o wg0 -j ACCEPT;
36 | `.split('\n').join(' ');
37 | module.exports.LANG = process.env.LANGUAGE || 'en';
38 | module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
39 | module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
40 |
41 | const getRandomInt = (min, max) => min + Math.floor(Math.random() * (max - min));
42 | const getRandomJunkSize = () => getRandomInt(15, 150);
43 | const getRandomHeader = () => getRandomInt(1, 2_147_483_647);
44 |
45 | module.exports.JC = process.env.JC || getRandomInt(3, 10);
46 | module.exports.JMIN = process.env.JMIN || 50;
47 | module.exports.JMAX = process.env.JMAX || 1000;
48 | module.exports.S1 = process.env.S1 || getRandomJunkSize();
49 | module.exports.S2 = process.env.S2 || getRandomJunkSize();
50 | module.exports.H1 = process.env.H1 || getRandomHeader();
51 | module.exports.H2 = process.env.H2 || getRandomHeader();
52 | module.exports.H3 = process.env.H3 || getRandomHeader();
53 | module.exports.H4 = process.env.H4 || getRandomHeader();
54 |
--------------------------------------------------------------------------------
/src/www/js/api.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable no-undef */
3 |
4 | 'use strict';
5 |
6 | class API {
7 |
8 | async call({ method, path, body }) {
9 | const res = await fetch(`./api${path}`, {
10 | method,
11 | headers: {
12 | 'Content-Type': 'application/json',
13 | },
14 | body: body
15 | ? JSON.stringify(body)
16 | : undefined,
17 | });
18 |
19 | if (res.status === 204) {
20 | return undefined;
21 | }
22 |
23 | const json = await res.json();
24 |
25 | if (!res.ok) {
26 | throw new Error(json.error || res.statusText);
27 | }
28 |
29 | return json;
30 | }
31 |
32 | async getCheckUpdate() {
33 | return this.call({
34 | method: 'get',
35 | path: '/check-update',
36 | });
37 | }
38 |
39 | async getRelease() {
40 | return this.call({
41 | method: 'get',
42 | path: '/release',
43 | });
44 | }
45 |
46 | async getLang() {
47 | return this.call({
48 | method: 'get',
49 | path: '/lang',
50 | });
51 | }
52 |
53 | async getuiTrafficStats() {
54 | return this.call({
55 | method: 'get',
56 | path: '/ui-traffic-stats',
57 | });
58 | }
59 |
60 | async getChartType() {
61 | return this.call({
62 | method: 'get',
63 | path: '/ui-chart-type',
64 | });
65 | }
66 |
67 | async getSession() {
68 | return this.call({
69 | method: 'get',
70 | path: '/session',
71 | });
72 | }
73 |
74 | async createSession({ password }) {
75 | return this.call({
76 | method: 'post',
77 | path: '/session',
78 | body: { password },
79 | });
80 | }
81 |
82 | async deleteSession() {
83 | return this.call({
84 | method: 'delete',
85 | path: '/session',
86 | });
87 | }
88 |
89 | async getClients() {
90 | return this.call({
91 | method: 'get',
92 | path: '/wireguard/client',
93 | }).then((clients) => clients.map((client) => ({
94 | ...client,
95 | createdAt: new Date(client.createdAt),
96 | updatedAt: new Date(client.updatedAt),
97 | latestHandshakeAt: client.latestHandshakeAt !== null
98 | ? new Date(client.latestHandshakeAt)
99 | : null,
100 | })));
101 | }
102 |
103 | async createClient({ name }) {
104 | return this.call({
105 | method: 'post',
106 | path: '/wireguard/client',
107 | body: { name },
108 | });
109 | }
110 |
111 | async deleteClient({ clientId }) {
112 | return this.call({
113 | method: 'delete',
114 | path: `/wireguard/client/${clientId}`,
115 | });
116 | }
117 |
118 | async enableClient({ clientId }) {
119 | return this.call({
120 | method: 'post',
121 | path: `/wireguard/client/${clientId}/enable`,
122 | });
123 | }
124 |
125 | async disableClient({ clientId }) {
126 | return this.call({
127 | method: 'post',
128 | path: `/wireguard/client/${clientId}/disable`,
129 | });
130 | }
131 |
132 | async updateClientName({ clientId, name }) {
133 | return this.call({
134 | method: 'put',
135 | path: `/wireguard/client/${clientId}/name/`,
136 | body: { name },
137 | });
138 | }
139 |
140 | async updateClientAddress({ clientId, address }) {
141 | return this.call({
142 | method: 'put',
143 | path: `/wireguard/client/${clientId}/address/`,
144 | body: { address },
145 | });
146 | }
147 |
148 | }
149 |
--------------------------------------------------------------------------------
/src/www/js/vendor/vue-apexcharts.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Minified by jsDelivr using Terser v5.7.1.
3 | * Original file: /npm/vue-apexcharts@1.6.2/dist/vue-apexcharts.js
4 | *
5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 | */
7 | !function (t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e(require("apexcharts/dist/apexcharts.min")) : "function" == typeof define && define.amd ? define(["apexcharts/dist/apexcharts.min"], e) : t.VueApexCharts = e(t.ApexCharts) }(this, (function (t) { "use strict"; function e(t) { return (e = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (t) { return typeof t } : function (t) { return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t })(t) } function n(t, e, n) { return e in t ? Object.defineProperty(t, e, { value: n, enumerable: !0, configurable: !0, writable: !0 }) : t[e] = n, t } t = t && t.hasOwnProperty("default") ? t.default : t; var i = { props: { options: { type: Object }, type: { type: String }, series: { type: Array, required: !0, default: function () { return [] } }, width: { default: "100%" }, height: { default: "auto" } }, data: function () { return { chart: null } }, beforeMount: function () { window.ApexCharts = t }, mounted: function () { this.init() }, created: function () { var t = this; this.$watch("options", (function (e) { !t.chart && e ? t.init() : t.chart.updateOptions(t.options) })), this.$watch("series", (function (e) { !t.chart && e ? t.init() : t.chart.updateSeries(t.series) }));["type", "width", "height"].forEach((function (e) { t.$watch(e, (function () { t.refresh() })) })) }, beforeDestroy: function () { this.chart && this.destroy() }, render: function (t) { return t("div") }, methods: { init: function () { var e = this, n = { chart: { type: this.type || this.options.chart.type || "line", height: this.height, width: this.width, events: {} }, series: this.series }; Object.keys(this.$listeners).forEach((function (t) { n.chart.events[t] = e.$listeners[t] })); var i = this.extend(this.options, n); return this.chart = new t(this.$el, i), this.chart.render() }, isObject: function (t) { return t && "object" === e(t) && !Array.isArray(t) && null != t }, extend: function (t, e) { var i = this; "function" != typeof Object.assign && (Object.assign = function (t) { if (null == t) throw new TypeError("Cannot convert undefined or null to object"); for (var e = Object(t), n = 1; n < arguments.length; n++) { var i = arguments[n]; if (null != i) for (var r in i) i.hasOwnProperty(r) && (e[r] = i[r]) } return e }); var r = Object.assign({}, t); return this.isObject(t) && this.isObject(e) && Object.keys(e).forEach((function (o) { i.isObject(e[o]) && o in t ? r[o] = i.extend(t[o], e[o]) : Object.assign(r, n({}, o, e[o])) })), r }, refresh: function () { return this.destroy(), this.init() }, destroy: function () { this.chart.destroy() }, updateSeries: function (t, e) { return this.chart.updateSeries(t, e) }, updateOptions: function (t, e, n, i) { return this.chart.updateOptions(t, e, n, i) }, toggleSeries: function (t) { return this.chart.toggleSeries(t) }, showSeries: function (t) { this.chart.showSeries(t) }, hideSeries: function (t) { this.chart.hideSeries(t) }, appendSeries: function (t, e) { return this.chart.appendSeries(t, e) }, resetSeries: function () { this.chart.resetSeries() }, zoomX: function (t, e) { this.chart.zoomX(t, e) }, toggleDataPointSelection: function (t, e) { this.chart.toggleDataPointSelection(t, e) }, appendData: function (t) { return this.chart.appendData(t) }, addText: function (t) { this.chart.addText(t) }, addImage: function (t) { this.chart.addImage(t) }, addShape: function (t) { this.chart.addShape(t) }, dataURI: function () { return this.chart.dataURI() }, setLocale: function (t) { return this.chart.setLocale(t) }, addXaxisAnnotation: function (t, e) { this.chart.addXaxisAnnotation(t, e) }, addYaxisAnnotation: function (t, e) { this.chart.addYaxisAnnotation(t, e) }, addPointAnnotation: function (t, e) { this.chart.addPointAnnotation(t, e) }, removeAnnotation: function (t, e) { this.chart.removeAnnotation(t, e) }, clearAnnotations: function () { this.chart.clearAnnotations() } } }; return window.ApexCharts = t, i.install = function (e) { e.ApexCharts = t, window.ApexCharts = t, Object.defineProperty(e.prototype, "$apexcharts", { get: function () { return t } }) }, i }));
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to amnezia-wg-easy
2 |
3 | First and foremost, thank you! We appreciate that you want to contribute to amnezia-wg-easy, your time is valuable, and your contributions mean a lot to us.
4 |
5 |
6 | ## Important!
7 |
8 | By contributing to this project, you:
9 |
10 | * Agree that you have authored 100% of the content
11 | * Agree that you have the necessary rights to the content
12 | * Agree that you have received the necessary permissions from your employer to make the contributions (if applicable)
13 | * Agree that the content you contribute may be provided under the Project license(s)
14 | * 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.
15 |
16 |
17 | ## Getting started
18 |
19 | **What does "contributing" mean?**
20 |
21 | Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following:
22 |
23 | - Updating or correcting documentation
24 | - Feature requests
25 | - Bug reports
26 |
27 |
28 | ## Showing support for amnezia-wg-easy
29 |
30 | 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.
31 |
32 | Don't have time to contribute? No worries, here are some other ways to show your support for amnezia-wg-easy:
33 |
34 | - star the [project](https://github.com/spcfox/amnezia-wg-easy)
35 |
36 |
37 | ## Issues
38 |
39 | 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.
40 |
41 | Do not create issues about bumping dependencies unless a bug has been identified and you can demonstrate that it effects this library.
42 |
43 | **Help us to help you**
44 |
45 | Remember that we’re here to help, but not to make guesses about what you need help with:
46 |
47 | - Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you.
48 | - 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.
49 |
50 | _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._
51 |
52 | ### Before creating an issue
53 |
54 | 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.
55 |
56 | Try to follow these guidelines:
57 |
58 | - **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]
59 | - **Investigate the issue** - Search for exising issues (open or closed) that address the issue, and might have even resolved it already.
60 | - **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue.
61 | - Create the issue in the appropriate repository.
62 |
63 | ### Creating an issue
64 |
65 | 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:
66 |
67 | - **description**: (required) What is the bug you're experiencing? How are you using this library/app?
68 | - **OS**: (required) what operating system are you on?
69 | - **version**: (required) please note the version of amnezia-wg-easy are you using
70 | - **error messages**: (required) please paste any error messages into the issue, or a [gist](https://gist.github.com/)
71 | - **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using
72 |
73 |
74 | ### Closing issues
75 |
76 | The original poster or the maintainers of amnezia-wg-easy may close an issue at any time. Typically, but not exclusively, issues are closed when:
77 |
78 | - The issue is resolved
79 | - The project's maintainers have determined the issue is out of scope
80 | - An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked.
81 | - A discussion has clearly run its course
82 |
83 |
84 | ## Next steps
85 |
86 | **Tips for creating idiomatic issues**
87 |
88 | 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.
89 |
90 | The following resources were hand-picked to help you be the most effective contributor you can be:
91 |
92 | - 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.
93 | - 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)
94 | - The GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/) is another great markdown resource.
95 | - 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/).
96 |
97 | At the very least, please try to:
98 |
99 | - 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
100 | - When applicable, use syntax highlighting by adding the correct language name after the first "code fence"
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AmnewziaWG Easy
2 |
3 | You have found the easiest way to install & manage AmneziaWG on any Linux host!
4 |
5 |
6 |
7 |
8 |
9 | ## Features
10 |
11 | * All-in-one: AmneziaWG + Web UI.
12 | * Easy installation, simple to use.
13 | * List, create, edit, delete, enable & disable clients.
14 | * Download a client's configuration file.
15 | * Statistics for which clients are connected.
16 | * Tx/Rx charts for each connected client.
17 | * Gravatar support.
18 | * Automatic Light / Dark Mode
19 | * Multilanguage Support
20 | * UI_TRAFFIC_STATS (default off)
21 |
22 | ## Requirements
23 |
24 | * A host with Docker installed.
25 |
26 | ## Versions
27 |
28 | We provide more then 1 docker image to get, this will help you decide which one is best for you.
29 |
30 | | tag | Branch | Example | Description |
31 | | - | - | - | - |
32 | | `latest` | production | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possbile get bug fixes quickly when needed, deployed against `production`. |
33 | | `13` | production | `ghcr.io/wg-easy/wg-easy:13` | same as latest, stick to a version tag. |
34 | | `nightly` | master | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against `master`. |
35 | | `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into `master`. |
36 |
37 | ## Installation
38 |
39 | ### 1. Install Docker
40 |
41 | If you haven't installed Docker yet, install it by running:
42 |
43 | ```bash
44 | curl -sSL https://get.docker.com | sh
45 | sudo usermod -aG docker $(whoami)
46 | exit
47 | ```
48 |
49 | And log in again.
50 |
51 | ### 2. Run AmneziaWG Easy
52 |
53 | ```
54 | docker run -d \
55 | --name=amnezia-wg-easy \
56 | -e LANGUAGE=en \
57 | -e WG_HOST=<🚨YOUR_SERVER_IP> \
58 | -e PASSWORD=<🚨YOUR_ADMIN_PASSWORD> \
59 | -e PORT=51821 \
60 | -e WG_PORT=51820 \
61 | -v ~/.amnezia-wg-easy:/etc/wireguard \
62 | -p 51820:51820/udp \
63 | -p 51821:51821/tcp \
64 | --cap-add=NET_ADMIN \
65 | --cap-add=SYS_MODULE \
66 | --sysctl="net.ipv4.conf.all.src_valid_mark=1" \
67 | --sysctl="net.ipv4.ip_forward=1" \
68 | --device=/dev/net/tun:/dev/net/tun \
69 | --restart unless-stopped \
70 | ghcr.io/spcfox/amnezia-wg-easy
71 | ```
72 |
73 | > 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
74 | >
75 | > 💡 Replace `YOUR_ADMIN_PASSWORD` with a password to log in on the Web UI.
76 |
77 | The Web UI will now be available on `http://0.0.0.0:51821`.
78 |
79 | > 💡 Your configuration files will be saved in `~/.amnezia-wg-easy`
80 |
81 | AmneziaWG Easy can be launched with Docker Compose as well - just download
82 | [`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
83 | execute `docker compose up --detach`.
84 |
85 | ## Options
86 |
87 | These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
88 |
89 | | Env | Default | Example | Description |
90 | | - | - | - | - |
91 | | `LANGUAGE` | `en` | `de` | Web UI language (Supports: en, ru, tr, no, pl, fr, de, ca, es). |
92 | | `CHECK_UPDATE` | `true` | `false` | Check for a new version and display a notification about its availability |
93 | | `PORT` | `51821` | `6789` | TCP port for Web UI. |
94 | | `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
95 | | `PASSWORD` | - | `foobar123` | When set, requires a password when logging in to the Web UI. |
96 | | `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
97 | | `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the AmneziaWG traffic should be forwarded through. |
98 | | `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. AmneziaWG will listen on that (othwise default) inside the Docker container. |
99 | | `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
100 | | `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. |
101 | | `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
102 | | `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. |
103 | | `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
104 | | `WG_PRE_UP` | `...` | - | See [config.js](/src/config.js#L21) for the default value. |
105 | | `WG_POST_UP` | `...` | `iptables ...` | See [config.js](/src/config.js#L22) for the default value. |
106 | | `WG_PRE_DOWN` | `...` | - | See [config.js](/src/config.js#L29) for the default value. |
107 | | `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](/src/config.js#L30) for the default value. |
108 | | `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
109 | | `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 |
110 | | `JC` | `random` | `5` | Junk packet count — number of packets with random data that are sent before the start of the session. |
111 | | `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. |
112 | | `JMAX` | `1000` | `250` | Junk packet maximum size — maximum size for Junk packets. |
113 | | `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. |
114 | | `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. |
115 | | `H1` | `random` | `1234567891` | Init packet magic header — the header of the first byte of the handshake. Must be < uint_max. |
116 | | `H2` | `random` | `1234567892` | Response packet magic header — header of the first byte of the handshake response. Must be < uint_max. |
117 | | `H3` | `random` | `1234567893` | Underload packet magic header — UnderLoad packet header. Must be < uint_max. |
118 | | `H4` | `random` | `1234567894` | Transport packet magic header — header of the packet of the data packet. Must be < uint_max. |
119 |
120 | > If you change `WG_PORT`, make sure to also change the exposed port.
121 |
122 | ## Updating
123 |
124 | To update to the latest version, simply run:
125 |
126 | ```bash
127 | docker stop amnezia-wg-easy
128 | docker rm amnezia-wg-easy
129 | docker pull ghcr.io/spcfox/amnezia-wg-easy
130 | ```
131 |
132 | And then run the `docker run -d \ ...` command above again.
133 |
134 | With Docker Compose AmneziaWG Easy can be updated with a single command:
135 | `docker compose up --detach --pull always` (if an image tag is specified in the
136 | Compose file and it is not `latest`, make sure that it is changed to the desired
137 | one; by default it is omitted and
138 | [defaults to `latest`](https://docs.docker.com/engine/reference/run/#image-references)). \
139 | The WireGuared Easy container will be automatically recreated if a newer image
140 | was pulled.
141 |
142 | ## Thanks
143 |
144 | Based on [wg-easy](https://github.com/wg-easy/wg-easy) by Emile Nijssen.
145 |
--------------------------------------------------------------------------------
/src/www/js/vendor/sha256.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * [js-sha256]{@link https://github.com/emn178/js-sha256}
3 | *
4 | * @version 0.11.0
5 | * @author Chen, Yi-Cyuan [emn178@gmail.com]
6 | * @copyright Chen, Yi-Cyuan 2014-2024
7 | * @license MIT
8 | */
9 | !function(){"use strict";function t(t,i){i?(d[0]=d[16]=d[1]=d[2]=d[3]=d[4]=d[5]=d[6]=d[7]=d[8]=d[9]=d[10]=d[11]=d[12]=d[13]=d[14]=d[15]=0,this.blocks=d):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],t?(this.h0=3238371032,this.h1=914150663,this.h2=812702999,this.h3=4144912697,this.h4=4290775857,this.h5=1750603025,this.h6=1694076839,this.h7=3204075428):(this.h0=1779033703,this.h1=3144134277,this.h2=1013904242,this.h3=2773480762,this.h4=1359893119,this.h5=2600822924,this.h6=528734635,this.h7=1541459225),this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0,this.is224=t}function i(i,r,s){var e,n=typeof i;if("string"===n){var o,a=[],u=i.length,c=0;for(e=0;e>>6,a[c++]=128|63&o):o<55296||o>=57344?(a[c++]=224|o>>>12,a[c++]=128|o>>>6&63,a[c++]=128|63&o):(o=65536+((1023&o)<<10|1023&i.charCodeAt(++e)),a[c++]=240|o>>>18,a[c++]=128|o>>>12&63,a[c++]=128|o>>>6&63,a[c++]=128|63&o);i=a}else{if("object"!==n)throw new Error(h);if(null===i)throw new Error(h);if(f&&i.constructor===ArrayBuffer)i=new Uint8Array(i);else if(!(Array.isArray(i)||f&&ArrayBuffer.isView(i)))throw new Error(h)}i.length>64&&(i=new t(r,!0).update(i).array());var y=[],p=[];for(e=0;e<64;++e){var l=i[e]||0;y[e]=92^l,p[e]=54^l}t.call(this,r,s),this.update(p),this.oKeyPad=y,this.inner=!0,this.sharedMemory=s}var h="input is invalid type",r="object"==typeof window,s=r?window:{};s.JS_SHA256_NO_WINDOW&&(r=!1);var e=!r&&"object"==typeof self,n=!s.JS_SHA256_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node;n?s=global:e&&(s=self);var o=!s.JS_SHA256_NO_COMMON_JS&&"object"==typeof module&&module.exports,a="function"==typeof define&&define.amd,f=!s.JS_SHA256_NO_ARRAY_BUFFER&&"undefined"!=typeof ArrayBuffer,u="0123456789abcdef".split(""),c=[-2147483648,8388608,32768,128],y=[24,16,8,0],p=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],l=["hex","array","digest","arrayBuffer"],d=[];!s.JS_SHA256_NO_NODE_JS&&Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),!f||!s.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW&&ArrayBuffer.isView||(ArrayBuffer.isView=function(t){return"object"==typeof t&&t.buffer&&t.buffer.constructor===ArrayBuffer});var A=function(i,h){return function(r){return new t(h,!0).update(r)[i]()}},w=function(i){var h=A("hex",i);n&&(h=b(h,i)),h.create=function(){return new t(i)},h.update=function(t){return h.create().update(t)};for(var r=0;r>>2]|=t[n]<>>2]|=s<>>2]|=(192|s>>>6)<>>2]|=(128|63&s)<=57344?(a[e>>>2]|=(224|s>>>12)<>>2]|=(128|s>>>6&63)<>>2]|=(128|63&s)<>>2]|=(240|s>>>18)<>>2]|=(128|s>>>12&63)<>>2]|=(128|s>>>6&63)<>>2]|=(128|63&s)<=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,i=this.lastByteIndex;t[16]=this.block,t[i>>>2]|=c[3&i],this.block=t[16],i>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,i,h,r,s,e,n,o,a,f=this.h0,u=this.h1,c=this.h2,y=this.h3,l=this.h4,d=this.h5,A=this.h6,w=this.h7,b=this.blocks;for(t=16;t<64;++t)i=((s=b[t-15])>>>7|s<<25)^(s>>>18|s<<14)^s>>>3,h=((s=b[t-2])>>>17|s<<15)^(s>>>19|s<<13)^s>>>10,b[t]=b[t-16]+i+b[t-7]+h<<0;for(a=u&c,t=0;t<64;t+=4)this.first?(this.is224?(e=300032,w=(s=b[0]-1413257819)-150054599<<0,y=s+24177077<<0):(e=704751109,w=(s=b[0]-210244248)-1521486534<<0,y=s+143694565<<0),this.first=!1):(i=(f>>>2|f<<30)^(f>>>13|f<<19)^(f>>>22|f<<10),r=(e=f&u)^f&c^a,w=y+(s=w+(h=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7))+(l&d^~l&A)+p[t]+b[t])<<0,y=s+(i+r)<<0),i=(y>>>2|y<<30)^(y>>>13|y<<19)^(y>>>22|y<<10),r=(n=y&f)^y&u^e,A=c+(s=A+(h=(w>>>6|w<<26)^(w>>>11|w<<21)^(w>>>25|w<<7))+(w&l^~w&d)+p[t+1]+b[t+1])<<0,i=((c=s+(i+r)<<0)>>>2|c<<30)^(c>>>13|c<<19)^(c>>>22|c<<10),r=(o=c&y)^c&f^n,d=u+(s=d+(h=(A>>>6|A<<26)^(A>>>11|A<<21)^(A>>>25|A<<7))+(A&w^~A&l)+p[t+2]+b[t+2])<<0,i=((u=s+(i+r)<<0)>>>2|u<<30)^(u>>>13|u<<19)^(u>>>22|u<<10),r=(a=u&c)^u&y^o,l=f+(s=l+(h=(d>>>6|d<<26)^(d>>>11|d<<21)^(d>>>25|d<<7))+(d&A^~d&w)+p[t+3]+b[t+3])<<0,f=s+(i+r)<<0,this.chromeBugWorkAround=!0;this.h0=this.h0+f<<0,this.h1=this.h1+u<<0,this.h2=this.h2+c<<0,this.h3=this.h3+y<<0,this.h4=this.h4+l<<0,this.h5=this.h5+d<<0,this.h6=this.h6+A<<0,this.h7=this.h7+w<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,i=this.h1,h=this.h2,r=this.h3,s=this.h4,e=this.h5,n=this.h6,o=this.h7,a=u[t>>>28&15]+u[t>>>24&15]+u[t>>>20&15]+u[t>>>16&15]+u[t>>>12&15]+u[t>>>8&15]+u[t>>>4&15]+u[15&t]+u[i>>>28&15]+u[i>>>24&15]+u[i>>>20&15]+u[i>>>16&15]+u[i>>>12&15]+u[i>>>8&15]+u[i>>>4&15]+u[15&i]+u[h>>>28&15]+u[h>>>24&15]+u[h>>>20&15]+u[h>>>16&15]+u[h>>>12&15]+u[h>>>8&15]+u[h>>>4&15]+u[15&h]+u[r>>>28&15]+u[r>>>24&15]+u[r>>>20&15]+u[r>>>16&15]+u[r>>>12&15]+u[r>>>8&15]+u[r>>>4&15]+u[15&r]+u[s>>>28&15]+u[s>>>24&15]+u[s>>>20&15]+u[s>>>16&15]+u[s>>>12&15]+u[s>>>8&15]+u[s>>>4&15]+u[15&s]+u[e>>>28&15]+u[e>>>24&15]+u[e>>>20&15]+u[e>>>16&15]+u[e>>>12&15]+u[e>>>8&15]+u[e>>>4&15]+u[15&e]+u[n>>>28&15]+u[n>>>24&15]+u[n>>>20&15]+u[n>>>16&15]+u[n>>>12&15]+u[n>>>8&15]+u[n>>>4&15]+u[15&n];return this.is224||(a+=u[o>>>28&15]+u[o>>>24&15]+u[o>>>20&15]+u[o>>>16&15]+u[o>>>12&15]+u[o>>>8&15]+u[o>>>4&15]+u[15&o]),a},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,i=this.h1,h=this.h2,r=this.h3,s=this.h4,e=this.h5,n=this.h6,o=this.h7,a=[t>>>24&255,t>>>16&255,t>>>8&255,255&t,i>>>24&255,i>>>16&255,i>>>8&255,255&i,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r,s>>>24&255,s>>>16&255,s>>>8&255,255&s,e>>>24&255,e>>>16&255,e>>>8&255,255&e,n>>>24&255,n>>>16&255,n>>>8&255,255&n];return this.is224||a.push(o>>>24&255,o>>>16&255,o>>>8&255,255&o),a},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(this.is224?28:32),i=new DataView(t);return i.setUint32(0,this.h0),i.setUint32(4,this.h1),i.setUint32(8,this.h2),i.setUint32(12,this.h3),i.setUint32(16,this.h4),i.setUint32(20,this.h5),i.setUint32(24,this.h6),this.is224||i.setUint32(28,this.h7),t},(i.prototype=new t).finalize=function(){if(t.prototype.finalize.call(this),this.inner){this.inner=!1;var i=this.array();t.call(this,this.is224,this.sharedMemory),this.update(this.oKeyPad),this.update(i),t.prototype.finalize.call(this)}};var B=w();B.sha256=B,B.sha224=w(!0),B.sha256.hmac=v(),B.sha224.hmac=v(!0),o?module.exports=B:(s.sha256=B.sha256,s.sha224=B.sha224,a&&define(function(){return B}))}();
--------------------------------------------------------------------------------
/src/lib/Server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const crypto = require('node:crypto');
4 | const { createServer } = require('node:http');
5 | const { stat, readFile } = require('node:fs/promises');
6 | const { resolve, sep } = require('node:path');
7 |
8 | const expressSession = require('express-session');
9 | const debug = require('debug')('Server');
10 |
11 | const {
12 | createApp,
13 | createError,
14 | createRouter,
15 | defineEventHandler,
16 | fromNodeMiddleware,
17 | getRouterParam,
18 | toNodeListener,
19 | readBody,
20 | setHeader,
21 | serveStatic,
22 | } = require('h3');
23 |
24 | const WireGuard = require('../services/WireGuard');
25 |
26 | const {
27 | CHECK_UPDATE,
28 | PORT,
29 | WEBUI_HOST,
30 | RELEASE,
31 | PASSWORD,
32 | LANG,
33 | UI_TRAFFIC_STATS,
34 | UI_CHART_TYPE,
35 | } = require('../config');
36 |
37 | module.exports = class Server {
38 |
39 | constructor() {
40 | const app = createApp();
41 | this.app = app;
42 |
43 | app.use(fromNodeMiddleware(expressSession({
44 | secret: crypto.randomBytes(256).toString('hex'),
45 | resave: true,
46 | saveUninitialized: true,
47 | })));
48 |
49 | const router = createRouter();
50 | app.use(router);
51 |
52 | router
53 | .get('/api/release', defineEventHandler((event) => {
54 | setHeader(event, 'Content-Type', 'application/json');
55 | return RELEASE;
56 | }))
57 |
58 | .get('/api/check-update', defineEventHandler((event) => {
59 | setHeader(event, 'Content-Type', 'application/json');
60 | return CHECK_UPDATE;
61 | }))
62 |
63 | .get('/api/lang', defineEventHandler((event) => {
64 | setHeader(event, 'Content-Type', 'application/json');
65 | return `"${LANG}"`;
66 | }))
67 |
68 | .get('/api/ui-traffic-stats', defineEventHandler((event) => {
69 | setHeader(event, 'Content-Type', 'application/json');
70 | return UI_TRAFFIC_STATS;
71 | }))
72 |
73 | .get('/api/ui-chart-type', defineEventHandler((event) => {
74 | setHeader(event, 'Content-Type', 'application/json');
75 | return `"${UI_CHART_TYPE}"`;
76 | }))
77 |
78 | // Authentication
79 | .get('/api/session', defineEventHandler((event) => {
80 | const requiresPassword = !!process.env.PASSWORD;
81 | const authenticated = requiresPassword
82 | ? !!(event.node.req.session && event.node.req.session.authenticated)
83 | : true;
84 |
85 | return {
86 | requiresPassword,
87 | authenticated,
88 | };
89 | }))
90 | .post('/api/session', defineEventHandler(async (event) => {
91 | const { password } = await readBody(event);
92 |
93 | if (typeof password !== 'string') {
94 | throw createError({
95 | status: 401,
96 | message: 'Missing: Password',
97 | });
98 | }
99 |
100 | if (password !== PASSWORD) {
101 | throw createError({
102 | status: 401,
103 | message: 'Incorrect Password',
104 | });
105 | }
106 |
107 | event.node.req.session.authenticated = true;
108 | event.node.req.session.save();
109 |
110 | debug(`New Session: ${event.node.req.session.id}`);
111 |
112 | return { succcess: true };
113 | }));
114 |
115 | // WireGuard
116 | app.use(
117 | fromNodeMiddleware((req, res, next) => {
118 | if (!PASSWORD || !req.url.startsWith('/api/')) {
119 | return next();
120 | }
121 |
122 | if (req.session && req.session.authenticated) {
123 | return next();
124 | }
125 |
126 | return res.status(401).json({
127 | error: 'Not Logged In',
128 | });
129 | }),
130 | );
131 |
132 | const router2 = createRouter();
133 | app.use(router2);
134 |
135 | router2
136 | .delete('/api/session', defineEventHandler((event) => {
137 | const sessionId = event.node.req.session.id;
138 |
139 | event.node.req.session.destroy();
140 |
141 | debug(`Deleted Session: ${sessionId}`);
142 | return { success: true };
143 | }))
144 | .get('/api/wireguard/client', defineEventHandler(() => {
145 | return WireGuard.getClients();
146 | }))
147 | .get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => {
148 | const clientId = getRouterParam(event, 'clientId');
149 | const svg = await WireGuard.getClientQRCodeSVG({ clientId });
150 | setHeader(event, 'Content-Type', 'image/svg+xml');
151 | return svg;
152 | }))
153 | .get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => {
154 | const clientId = getRouterParam(event, 'clientId');
155 | const client = await WireGuard.getClient({ clientId });
156 | const config = await WireGuard.getClientConfiguration({ clientId });
157 | const configName = client.name
158 | .replace(/[^a-zA-Z0-9_=+.-]/g, '-')
159 | .replace(/(-{2,}|-$)/g, '-')
160 | .replace(/-$/, '')
161 | .substring(0, 32);
162 | setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
163 | setHeader(event, 'Content-Type', 'text/plain');
164 | return config;
165 | }))
166 | .post('/api/wireguard/client', defineEventHandler(async (event) => {
167 | const { name } = await readBody(event);
168 | await WireGuard.createClient({ name });
169 | return { success: true };
170 | }))
171 | .delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
172 | const clientId = getRouterParam(event, 'clientId');
173 | await WireGuard.deleteClient({ clientId });
174 | return { success: true };
175 | }))
176 | .post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => {
177 | const clientId = getRouterParam(event, 'clientId');
178 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
179 | throw createError({ status: 403 });
180 | }
181 | await WireGuard.enableClient({ clientId });
182 | return { success: true };
183 | }))
184 | .post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
185 | const clientId = getRouterParam(event, 'clientId');
186 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
187 | throw createError({ status: 403 });
188 | }
189 | await WireGuard.disableClient({ clientId });
190 | return { success: true };
191 | }))
192 | .put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => {
193 | const clientId = getRouterParam(event, 'clientId');
194 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
195 | throw createError({ status: 403 });
196 | }
197 | const { name } = await readBody(event);
198 | await WireGuard.updateClientName({ clientId, name });
199 | return { success: true };
200 | }))
201 | .put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => {
202 | const clientId = getRouterParam(event, 'clientId');
203 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
204 | throw createError({ status: 403 });
205 | }
206 | const { address } = await readBody(event);
207 | await WireGuard.updateClientAddress({ clientId, address });
208 | return { success: true };
209 | }));
210 |
211 | const safePathJoin = (base, target) => {
212 | // Manage web root (edge case)
213 | if (target === '/') {
214 | return `${base}${sep}`;
215 | }
216 |
217 | // Prepend './' to prevent absolute paths
218 | const targetPath = `.${sep}${target}`;
219 |
220 | // Resolve the absolute path
221 | const resolvedPath = resolve(base, targetPath);
222 |
223 | // Check if resolvedPath is a subpath of base
224 | if (resolvedPath.startsWith(`${base}${sep}`)) {
225 | return resolvedPath;
226 | }
227 |
228 | throw createError({
229 | status: 400,
230 | message: 'Bad Request',
231 | });
232 | };
233 |
234 | // Static assets
235 | const publicDir = '/app/www';
236 | app.use(
237 | defineEventHandler((event) => {
238 | return serveStatic(event, {
239 | getContents: (id) => {
240 | return readFile(safePathJoin(publicDir, id));
241 | },
242 | getMeta: async (id) => {
243 | const filePath = safePathJoin(publicDir, id);
244 |
245 | const stats = await stat(filePath).catch(() => {});
246 | if (!stats || !stats.isFile()) {
247 | return;
248 | }
249 |
250 | if (id.endsWith('.html')) setHeader(event, 'Content-Type', 'text/html');
251 | if (id.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript');
252 | if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json');
253 | if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css');
254 | if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png');
255 | if (id.endsWith('.svg')) setHeader(event, 'Content-Type', 'image/svg+xml');
256 |
257 | return {
258 | size: stats.size,
259 | mtime: stats.mtimeMs,
260 | };
261 | },
262 | });
263 | }),
264 | );
265 |
266 | createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST);
267 | debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
268 | }
269 |
270 | };
271 |
--------------------------------------------------------------------------------
/src/lib/WireGuard.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('node:fs/promises');
4 | const path = require('path');
5 | const debug = require('debug')('WireGuard');
6 | const crypto = require('node:crypto');
7 | const QRCode = require('qrcode');
8 |
9 | const Util = require('./Util');
10 | const ServerError = require('./ServerError');
11 |
12 | const {
13 | WG_PATH,
14 | WG_HOST,
15 | WG_PORT,
16 | WG_MTU,
17 | WG_DEFAULT_DNS,
18 | WG_DEFAULT_ADDRESS,
19 | WG_PERSISTENT_KEEPALIVE,
20 | WG_ALLOWED_IPS,
21 | WG_PRE_UP,
22 | WG_POST_UP,
23 | WG_PRE_DOWN,
24 | WG_POST_DOWN,
25 | JC,
26 | JMIN,
27 | JMAX,
28 | S1,
29 | S2,
30 | H1,
31 | H2,
32 | H3,
33 | H4,
34 | } = require('../config');
35 |
36 | module.exports = class WireGuard {
37 |
38 | async getConfig() {
39 | if (!this.__configPromise) {
40 | this.__configPromise = Promise.resolve().then(async () => {
41 | if (!WG_HOST) {
42 | throw new Error('WG_HOST Environment Variable Not Set!');
43 | }
44 |
45 | debug('Loading configuration...');
46 | let config;
47 | try {
48 | config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
49 | config = JSON.parse(config);
50 | debug('Configuration loaded.');
51 | } catch (err) {
52 | const privateKey = await Util.exec('wg genkey');
53 | const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
54 | log: 'echo ***hidden*** | wg pubkey',
55 | });
56 |
57 | const address = WG_DEFAULT_ADDRESS.replace('x', '1');
58 |
59 | config = {
60 | server: {
61 | privateKey,
62 | publicKey,
63 | address,
64 | jc: JC,
65 | jmin: JMIN,
66 | jmax: JMAX,
67 | s1: S1,
68 | s2: S2,
69 | h1: H1,
70 | h2: H2,
71 | h3: H3,
72 | h4: H4,
73 | },
74 | clients: {},
75 | };
76 |
77 | debug('Configuration generated.');
78 | }
79 |
80 | await this.__saveConfig(config);
81 | await Util.exec('wg-quick down wg0').catch(() => { });
82 | await Util.exec('wg-quick up wg0').catch((err) => {
83 | if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
84 | throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
85 | }
86 |
87 | throw err;
88 | });
89 | // await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`);
90 | // await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
91 | // await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
92 | // await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
93 | await this.__syncConfig();
94 |
95 | return config;
96 | });
97 | }
98 |
99 | return this.__configPromise;
100 | }
101 |
102 | async saveConfig() {
103 | const config = await this.getConfig();
104 | await this.__saveConfig(config);
105 | await this.__syncConfig();
106 | }
107 |
108 | async __saveConfig(config) {
109 | let result = `
110 | # Note: Do not edit this file directly.
111 | # Your changes will be overwritten!
112 |
113 | # Server
114 | [Interface]
115 | PrivateKey = ${config.server.privateKey}
116 | Address = ${config.server.address}/24
117 | ListenPort = ${WG_PORT}
118 | PreUp = ${WG_PRE_UP}
119 | PostUp = ${WG_POST_UP}
120 | PreDown = ${WG_PRE_DOWN}
121 | PostDown = ${WG_POST_DOWN}
122 | Jc = ${config.server.jc}
123 | Jmin = ${config.server.jmin}
124 | Jmax = ${config.server.jmax}
125 | S1 = ${config.server.s1}
126 | S2 = ${config.server.s2}
127 | H1 = ${config.server.h1}
128 | H2 = ${config.server.h2}
129 | H3 = ${config.server.h3}
130 | H4 = ${config.server.h4}
131 | Jc = ${config.server.jc}
132 | `;
133 |
134 | for (const [clientId, client] of Object.entries(config.clients)) {
135 | if (!client.enabled) continue;
136 |
137 | result += `
138 |
139 | # Client: ${client.name} (${clientId})
140 | [Peer]
141 | PublicKey = ${client.publicKey}
142 | ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
143 | }AllowedIPs = ${client.address}/32`;
144 | }
145 |
146 | debug('Config saving...');
147 | await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(config, false, 2), {
148 | mode: 0o660,
149 | });
150 | await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
151 | mode: 0o600,
152 | });
153 | debug('Config saved.');
154 | }
155 |
156 | async __syncConfig() {
157 | debug('Config syncing...');
158 | await Util.exec('wg syncconf wg0 <(wg-quick strip wg0)');
159 | debug('Config synced.');
160 | }
161 |
162 | async getClients() {
163 | const config = await this.getConfig();
164 | const clients = Object.entries(config.clients).map(([clientId, client]) => ({
165 | id: clientId,
166 | name: client.name,
167 | enabled: client.enabled,
168 | address: client.address,
169 | publicKey: client.publicKey,
170 | createdAt: new Date(client.createdAt),
171 | updatedAt: new Date(client.updatedAt),
172 | allowedIPs: client.allowedIPs,
173 | downloadableConfig: 'privateKey' in client,
174 | persistentKeepalive: null,
175 | latestHandshakeAt: null,
176 | transferRx: null,
177 | transferTx: null,
178 | }));
179 |
180 | // Loop WireGuard status
181 | const dump = await Util.exec('wg show wg0 dump', {
182 | log: false,
183 | });
184 | dump
185 | .trim()
186 | .split('\n')
187 | .slice(1)
188 | .forEach((line) => {
189 | const [
190 | publicKey,
191 | preSharedKey, // eslint-disable-line no-unused-vars
192 | endpoint, // eslint-disable-line no-unused-vars
193 | allowedIps, // eslint-disable-line no-unused-vars
194 | latestHandshakeAt,
195 | transferRx,
196 | transferTx,
197 | persistentKeepalive,
198 | ] = line.split('\t');
199 |
200 | const client = clients.find((client) => client.publicKey === publicKey);
201 | if (!client) return;
202 |
203 | client.latestHandshakeAt = latestHandshakeAt === '0'
204 | ? null
205 | : new Date(Number(`${latestHandshakeAt}000`));
206 | client.transferRx = Number(transferRx);
207 | client.transferTx = Number(transferTx);
208 | client.persistentKeepalive = persistentKeepalive;
209 | });
210 |
211 | return clients;
212 | }
213 |
214 | async getClient({ clientId }) {
215 | const config = await this.getConfig();
216 | const client = config.clients[clientId];
217 | if (!client) {
218 | throw new ServerError(`Client Not Found: ${clientId}`, 404);
219 | }
220 |
221 | return client;
222 | }
223 |
224 | async getClientConfiguration({ clientId }) {
225 | const config = await this.getConfig();
226 | const client = await this.getClient({ clientId });
227 |
228 | return `
229 | [Interface]
230 | PrivateKey = ${client.privateKey ? `${client.privateKey}` : 'REPLACE_ME'}
231 | Address = ${client.address}
232 | ${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\
233 | ${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\
234 | Jc = ${config.server.jc}
235 | Jmin = ${config.server.jmin}
236 | Jmax = ${config.server.jmax}
237 | S1 = ${config.server.s1}
238 | S2 = ${config.server.s2}
239 | H1 = ${config.server.h1}
240 | H2 = ${config.server.h2}
241 | H3 = ${config.server.h3}
242 | H4 = ${config.server.h4}
243 |
244 | [Peer]
245 | PublicKey = ${config.server.publicKey}
246 | ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
247 | }AllowedIPs = ${WG_ALLOWED_IPS}
248 | PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
249 | Endpoint = ${WG_HOST}:${WG_PORT}`;
250 | }
251 |
252 | async getClientQRCodeSVG({ clientId }) {
253 | const config = await this.getClientConfiguration({ clientId });
254 | return QRCode.toString(config, {
255 | type: 'svg',
256 | width: 512,
257 | });
258 | }
259 |
260 | async createClient({ name }) {
261 | if (!name) {
262 | throw new Error('Missing: Name');
263 | }
264 |
265 | const config = await this.getConfig();
266 |
267 | const privateKey = await Util.exec('wg genkey');
268 | const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
269 | const preSharedKey = await Util.exec('wg genpsk');
270 |
271 | // Calculate next IP
272 | let address;
273 | for (let i = 2; i < 255; i++) {
274 | const client = Object.values(config.clients).find((client) => {
275 | return client.address === WG_DEFAULT_ADDRESS.replace('x', i);
276 | });
277 |
278 | if (!client) {
279 | address = WG_DEFAULT_ADDRESS.replace('x', i);
280 | break;
281 | }
282 | }
283 |
284 | if (!address) {
285 | throw new Error('Maximum number of clients reached.');
286 | }
287 |
288 | // Create Client
289 | const id = crypto.randomUUID();
290 | const client = {
291 | id,
292 | name,
293 | address,
294 | privateKey,
295 | publicKey,
296 | preSharedKey,
297 |
298 | createdAt: new Date(),
299 | updatedAt: new Date(),
300 |
301 | enabled: true,
302 | };
303 |
304 | config.clients[id] = client;
305 |
306 | await this.saveConfig();
307 |
308 | return client;
309 | }
310 |
311 | async deleteClient({ clientId }) {
312 | const config = await this.getConfig();
313 |
314 | if (config.clients[clientId]) {
315 | delete config.clients[clientId];
316 | await this.saveConfig();
317 | }
318 | }
319 |
320 | async enableClient({ clientId }) {
321 | const client = await this.getClient({ clientId });
322 |
323 | client.enabled = true;
324 | client.updatedAt = new Date();
325 |
326 | await this.saveConfig();
327 | }
328 |
329 | async disableClient({ clientId }) {
330 | const client = await this.getClient({ clientId });
331 |
332 | client.enabled = false;
333 | client.updatedAt = new Date();
334 |
335 | await this.saveConfig();
336 | }
337 |
338 | async updateClientName({ clientId, name }) {
339 | const client = await this.getClient({ clientId });
340 |
341 | client.name = name;
342 | client.updatedAt = new Date();
343 |
344 | await this.saveConfig();
345 | }
346 |
347 | async updateClientAddress({ clientId, address }) {
348 | const client = await this.getClient({ clientId });
349 |
350 | if (!Util.isValidIPv4(address)) {
351 | throw new ServerError(`Invalid Address: ${address}`, 400);
352 | }
353 |
354 | client.address = address;
355 | client.updatedAt = new Date();
356 |
357 | await this.saveConfig();
358 | }
359 |
360 | // Shutdown wireguard
361 | async Shutdown() {
362 | await Util.exec('wg-quick down wg0').catch(() => { });
363 | }
364 |
365 | };
366 |
--------------------------------------------------------------------------------
/src/www/js/app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* eslint-disable no-alert */
3 | /* eslint-disable no-undef */
4 | /* eslint-disable no-new */
5 |
6 | 'use strict';
7 |
8 | const CHANGELOG_URL = 'https://raw.githubusercontent.com/spcfox/amnezia-wg-easy/production/docs/changelog.json';
9 |
10 | function bytes(bytes, decimals, kib, maxunit) {
11 | kib = kib || false;
12 | if (bytes === 0) return '0 B';
13 | if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'NaN';
14 | const k = kib ? 1024 : 1000;
15 | const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2;
16 | const sizes = kib
17 | ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB']
18 | : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
19 | let i = Math.floor(Math.log(bytes) / Math.log(k));
20 | if (maxunit !== undefined) {
21 | const index = sizes.indexOf(maxunit);
22 | if (index !== -1) i = index;
23 | }
24 | // eslint-disable-next-line no-restricted-properties
25 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
26 | }
27 |
28 | const i18n = new VueI18n({
29 | locale: localStorage.getItem('lang') || 'en',
30 | fallbackLocale: 'en',
31 | messages,
32 | });
33 |
34 | const UI_CHART_TYPES = [
35 | { type: false, strokeWidth: 0 },
36 | { type: 'line', strokeWidth: 3 },
37 | { type: 'area', strokeWidth: 0 },
38 | { type: 'bar', strokeWidth: 0 },
39 | ];
40 |
41 | const CHART_COLORS = {
42 | rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' },
43 | tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' },
44 | gradient: { light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'] },
45 | };
46 |
47 | new Vue({
48 | el: '#app',
49 | components: {
50 | apexchart: VueApexCharts,
51 | },
52 | i18n,
53 | data: {
54 | authenticated: null,
55 | authenticating: false,
56 | password: null,
57 | requiresPassword: null,
58 |
59 | clients: null,
60 | clientsPersist: {},
61 | clientDelete: null,
62 | clientCreate: null,
63 | clientCreateName: '',
64 | clientEditName: null,
65 | clientEditNameId: null,
66 | clientEditAddress: null,
67 | clientEditAddressId: null,
68 | qrcode: null,
69 |
70 | currentRelease: null,
71 | latestRelease: null,
72 |
73 | uiTrafficStats: false,
74 |
75 | uiChartType: 0,
76 | uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
77 | uiTheme: localStorage.theme || 'auto',
78 | prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
79 |
80 | chartOptions: {
81 | chart: {
82 | background: 'transparent',
83 | stacked: false,
84 | toolbar: {
85 | show: false,
86 | },
87 | animations: {
88 | enabled: false,
89 | },
90 | parentHeightOffset: 0,
91 | sparkline: {
92 | enabled: true,
93 | },
94 | },
95 | colors: [],
96 | stroke: {
97 | curve: 'smooth',
98 | },
99 | fill: {
100 | type: 'gradient',
101 | gradient: {
102 | shade: 'dark',
103 | type: 'vertical',
104 | shadeIntensity: 0,
105 | gradientToColors: CHART_COLORS.gradient[this.theme],
106 | inverseColors: false,
107 | opacityTo: 0,
108 | stops: [0, 100],
109 | },
110 | },
111 | dataLabels: {
112 | enabled: false,
113 | },
114 | plotOptions: {
115 | bar: {
116 | horizontal: false,
117 | },
118 | },
119 | xaxis: {
120 | labels: {
121 | show: false,
122 | },
123 | axisTicks: {
124 | show: false,
125 | },
126 | axisBorder: {
127 | show: false,
128 | },
129 | },
130 | yaxis: {
131 | labels: {
132 | show: false,
133 | },
134 | min: 0,
135 | },
136 | tooltip: {
137 | enabled: false,
138 | },
139 | legend: {
140 | show: false,
141 | },
142 | grid: {
143 | show: false,
144 | padding: {
145 | left: -10,
146 | right: 0,
147 | bottom: -15,
148 | top: -15,
149 | },
150 | column: {
151 | opacity: 0,
152 | },
153 | xaxis: {
154 | lines: {
155 | show: false,
156 | },
157 | },
158 | },
159 | },
160 | },
161 | methods: {
162 | dateTime: (value) => {
163 | return new Intl.DateTimeFormat(undefined, {
164 | year: 'numeric',
165 | month: 'short',
166 | day: 'numeric',
167 | hour: 'numeric',
168 | minute: 'numeric',
169 | }).format(value);
170 | },
171 | async refresh({
172 | updateCharts = false,
173 | } = {}) {
174 | if (!this.authenticated) return;
175 |
176 | const clients = await this.api.getClients();
177 | this.clients = clients.map((client) => {
178 | if (client.name.includes('@') && client.name.includes('.')) {
179 | client.avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`;
180 | }
181 |
182 | if (!this.clientsPersist[client.id]) {
183 | this.clientsPersist[client.id] = {};
184 | this.clientsPersist[client.id].transferRxHistory = Array(50).fill(0);
185 | this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
186 | this.clientsPersist[client.id].transferTxHistory = Array(50).fill(0);
187 | this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
188 | }
189 |
190 | // Debug
191 | // client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
192 | // client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
193 | // client.latestHandshakeAt = new Date();
194 | // this.requiresPassword = true;
195 |
196 | this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
197 | this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
198 | this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
199 | this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
200 |
201 | if (updateCharts) {
202 | this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
203 | this.clientsPersist[client.id].transferRxHistory.shift();
204 |
205 | this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
206 | this.clientsPersist[client.id].transferTxHistory.shift();
207 |
208 | this.clientsPersist[client.id].transferTxSeries = [{
209 | name: 'Tx',
210 | data: this.clientsPersist[client.id].transferTxHistory,
211 | }];
212 |
213 | this.clientsPersist[client.id].transferRxSeries = [{
214 | name: 'Rx',
215 | data: this.clientsPersist[client.id].transferRxHistory,
216 | }];
217 |
218 | client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory;
219 | client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
220 | client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory);
221 |
222 | client.transferTxSeries = this.clientsPersist[client.id].transferTxSeries;
223 | client.transferRxSeries = this.clientsPersist[client.id].transferRxSeries;
224 | }
225 |
226 | client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent;
227 | client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent;
228 |
229 | client.hoverTx = this.clientsPersist[client.id].hoverTx;
230 | client.hoverRx = this.clientsPersist[client.id].hoverRx;
231 |
232 | return client;
233 | });
234 | },
235 | login(e) {
236 | e.preventDefault();
237 |
238 | if (!this.password) return;
239 | if (this.authenticating) return;
240 |
241 | this.authenticating = true;
242 | this.api.createSession({
243 | password: this.password,
244 | })
245 | .then(async () => {
246 | const session = await this.api.getSession();
247 | this.authenticated = session.authenticated;
248 | this.requiresPassword = session.requiresPassword;
249 | return this.refresh();
250 | })
251 | .catch((err) => {
252 | alert(err.message || err.toString());
253 | })
254 | .finally(() => {
255 | this.authenticating = false;
256 | this.password = null;
257 | });
258 | },
259 | logout(e) {
260 | e.preventDefault();
261 |
262 | this.api.deleteSession()
263 | .then(() => {
264 | this.authenticated = false;
265 | this.clients = null;
266 | })
267 | .catch((err) => {
268 | alert(err.message || err.toString());
269 | });
270 | },
271 | createClient() {
272 | const name = this.clientCreateName;
273 | if (!name) return;
274 |
275 | this.api.createClient({ name })
276 | .catch((err) => alert(err.message || err.toString()))
277 | .finally(() => this.refresh().catch(console.error));
278 | },
279 | deleteClient(client) {
280 | this.api.deleteClient({ clientId: client.id })
281 | .catch((err) => alert(err.message || err.toString()))
282 | .finally(() => this.refresh().catch(console.error));
283 | },
284 | enableClient(client) {
285 | this.api.enableClient({ clientId: client.id })
286 | .catch((err) => alert(err.message || err.toString()))
287 | .finally(() => this.refresh().catch(console.error));
288 | },
289 | disableClient(client) {
290 | this.api.disableClient({ clientId: client.id })
291 | .catch((err) => alert(err.message || err.toString()))
292 | .finally(() => this.refresh().catch(console.error));
293 | },
294 | updateClientName(client, name) {
295 | this.api.updateClientName({ clientId: client.id, name })
296 | .catch((err) => alert(err.message || err.toString()))
297 | .finally(() => this.refresh().catch(console.error));
298 | },
299 | updateClientAddress(client, address) {
300 | this.api.updateClientAddress({ clientId: client.id, address })
301 | .catch((err) => alert(err.message || err.toString()))
302 | .finally(() => this.refresh().catch(console.error));
303 | },
304 | toggleTheme() {
305 | const themes = ['light', 'dark', 'auto'];
306 | const currentIndex = themes.indexOf(this.uiTheme);
307 | const newIndex = (currentIndex + 1) % themes.length;
308 | this.uiTheme = themes[newIndex];
309 | localStorage.theme = this.uiTheme;
310 | this.setTheme(this.uiTheme);
311 | },
312 | setTheme(theme) {
313 | const { classList } = document.documentElement;
314 | const shouldAddDarkClass = theme === 'dark' || (theme === 'auto' && this.prefersDarkScheme.matches);
315 | classList.toggle('dark', shouldAddDarkClass);
316 | },
317 | handlePrefersChange(e) {
318 | if (localStorage.theme === 'auto') {
319 | this.setTheme(e.matches ? 'dark' : 'light');
320 | }
321 | },
322 | toggleCharts() {
323 | localStorage.setItem('uiShowCharts', this.uiShowCharts ? 1 : 0);
324 | },
325 | },
326 | filters: {
327 | bytes,
328 | timeago: (value) => {
329 | return timeago.format(value, i18n.locale);
330 | },
331 | },
332 | mounted() {
333 | this.prefersDarkScheme.addListener(this.handlePrefersChange);
334 | this.setTheme(this.uiTheme);
335 |
336 | this.api = new API();
337 | this.api.getSession()
338 | .then((session) => {
339 | this.authenticated = session.authenticated;
340 | this.requiresPassword = session.requiresPassword;
341 | this.refresh({
342 | updateCharts: this.updateCharts,
343 | }).catch((err) => {
344 | alert(err.message || err.toString());
345 | });
346 | })
347 | .catch((err) => {
348 | alert(err.message || err.toString());
349 | });
350 |
351 | setInterval(() => {
352 | this.refresh({
353 | updateCharts: this.updateCharts,
354 | }).catch(console.error);
355 | }, 1000);
356 |
357 | this.api.getuiTrafficStats()
358 | .then((res) => {
359 | this.uiTrafficStats = res;
360 | })
361 | .catch(() => {
362 | this.uiTrafficStats = false;
363 | });
364 |
365 | this.api.getChartType()
366 | .then((res) => {
367 | this.uiChartType = parseInt(res, 10);
368 | })
369 | .catch(() => {
370 | this.uiChartType = 0;
371 | });
372 |
373 | Promise.resolve().then(async () => {
374 | const lang = await this.api.getLang();
375 | if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
376 | localStorage.setItem('lang', lang);
377 | i18n.locale = lang;
378 | }
379 |
380 | const checkUpdate = await this.api.getCheckUpdate();
381 | if (!checkUpdate) return;
382 |
383 | const currentRelease = await this.api.getRelease();
384 | const latestRelease = await fetch(CHANGELOG_URL)
385 | .then((res) => res.json())
386 | .then((releases) => {
387 | const releasesArray = Object.entries(releases).map(([version, changelog]) => ({
388 | version: parseInt(version, 10),
389 | changelog,
390 | }));
391 | releasesArray.sort((a, b) => {
392 | return b.version - a.version;
393 | });
394 |
395 | return releasesArray[0];
396 | });
397 |
398 | if (currentRelease >= latestRelease.version) return;
399 |
400 | this.currentRelease = currentRelease;
401 | this.latestRelease = latestRelease;
402 | }).catch((err) => console.error(err));
403 | },
404 | computed: {
405 | chartOptionsTX() {
406 | const opts = {
407 | ...this.chartOptions,
408 | colors: [CHART_COLORS.tx[this.theme]],
409 | };
410 | opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false;
411 | opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth;
412 | return opts;
413 | },
414 | chartOptionsRX() {
415 | const opts = {
416 | ...this.chartOptions,
417 | colors: [CHART_COLORS.rx[this.theme]],
418 | };
419 | opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false;
420 | opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth;
421 | return opts;
422 | },
423 | updateCharts() {
424 | return this.uiChartType > 0 && this.uiShowCharts;
425 | },
426 | theme() {
427 | if (this.uiTheme === 'auto') {
428 | return this.prefersDarkScheme.matches ? 'dark' : 'light';
429 | }
430 | return this.uiTheme;
431 | },
432 | },
433 | });
434 |
--------------------------------------------------------------------------------
/src/www/js/i18n.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const messages = { // eslint-disable-line no-unused-vars
4 | en: {
5 | name: 'Name',
6 | password: 'Password',
7 | signIn: 'Sign In',
8 | logout: 'Logout',
9 | updateAvailable: 'There is an update available!',
10 | update: 'Update',
11 | clients: 'Clients',
12 | new: 'New',
13 | deleteClient: 'Delete Client',
14 | deleteDialog1: 'Are you sure you want to delete',
15 | deleteDialog2: 'This action cannot be undone.',
16 | cancel: 'Cancel',
17 | create: 'Create',
18 | createdOn: 'Created on ',
19 | lastSeen: 'Last seen on ',
20 | totalDownload: 'Total Download: ',
21 | totalUpload: 'Total Upload: ',
22 | newClient: 'New Client',
23 | disableClient: 'Disable Client',
24 | enableClient: 'Enable Client',
25 | noClients: 'There are no clients yet.',
26 | noPrivKey: 'This client has no known private key. Cannot create Configuration.',
27 | showQR: 'Show QR Code',
28 | downloadConfig: 'Download Configuration',
29 | madeBy: 'Made by',
30 | donate: 'Donate',
31 | toggleCharts: 'Show/hide Charts',
32 | theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
33 | },
34 | ua: {
35 | name: 'Ім`я',
36 | password: 'Пароль',
37 | signIn: 'Увійти',
38 | logout: 'Вихід',
39 | updateAvailable: 'Доступне оновлення!',
40 | update: 'Оновити',
41 | clients: 'Клієнти',
42 | new: 'Новий',
43 | deleteClient: 'Видалити клієнта',
44 | deleteDialog1: 'Ви впевнені, що бажаєте видалити',
45 | deleteDialog2: 'Цю дію неможливо скасувати.',
46 | cancel: 'Скасувати',
47 | create: 'Створити',
48 | createdOn: 'Створено ',
49 | lastSeen: 'Останнє підключення в ',
50 | totalDownload: 'Всього завантажено: ',
51 | totalUpload: 'Всього відправлено: ',
52 | newClient: 'Новий клієнт',
53 | disableClient: 'Вимкнути клієнта',
54 | enableClient: 'Увімкнути клієнта',
55 | noClients: 'Ще немає клієнтів.',
56 | showQR: 'Показати QR-код',
57 | downloadConfig: 'Завантажити конфігурацію',
58 | madeBy: 'Зроблено',
59 | donate: 'Пожертвувати',
60 | },
61 | ru: {
62 | name: 'Имя',
63 | password: 'Пароль',
64 | signIn: 'Войти',
65 | logout: 'Выйти',
66 | updateAvailable: 'Доступно обновление!',
67 | update: 'Обновить',
68 | clients: 'Клиенты',
69 | new: 'Создать',
70 | deleteClient: 'Удалить клиента',
71 | deleteDialog1: 'Вы уверены, что хотите удалить',
72 | deleteDialog2: 'Это действие невозможно отменить.',
73 | cancel: 'Закрыть',
74 | create: 'Создать',
75 | createdOn: 'Создано в ',
76 | lastSeen: 'Последнее подключение в ',
77 | totalDownload: 'Всего скачано: ',
78 | totalUpload: 'Всего загружено: ',
79 | newClient: 'Создать клиента',
80 | disableClient: 'Выключить клиента',
81 | enableClient: 'Включить клиента',
82 | noClients: 'Пока нет клиентов.',
83 | showQR: 'Показать QR-код',
84 | downloadConfig: 'Скачать конфигурацию',
85 | madeBy: 'Автор',
86 | donate: 'Поблагодарить',
87 | },
88 | tr: { // Müslüm Barış Korkmazer @babico
89 | name: 'İsim',
90 | password: 'Şifre',
91 | signIn: 'Giriş Yap',
92 | logout: 'Çıkış Yap',
93 | updateAvailable: 'Mevcut bir güncelleme var!',
94 | update: 'Güncelle',
95 | clients: 'Kullanıcılar',
96 | new: 'Yeni',
97 | deleteClient: 'Kullanıcı Sil',
98 | deleteDialog1: 'Silmek istediğine emin misin',
99 | deleteDialog2: 'Bu işlem geri alınamaz.',
100 | cancel: 'İptal',
101 | create: 'Oluştur',
102 | createdAt: 'Şu saatte oluşturuldu: ',
103 | lastSeen: 'Son görülme tarihi: ',
104 | totalDownload: 'Toplam İndirme: ',
105 | totalUpload: 'Toplam Yükleme: ',
106 | newClient: 'Yeni Kullanıcı',
107 | disableClient: 'İstemciyi Devre Dışı Bırak',
108 | enableClient: 'İstemciyi Etkinleştir',
109 | noClients: 'Henüz kullanıcı yok.',
110 | showQR: 'QR Kodunu Göster',
111 | downloadConfig: 'Yapılandırmayı İndir',
112 | madeBy: 'Yapan Kişi: ',
113 | donate: 'Bağış Yap',
114 | changeLang: 'Dil Değiştir',
115 | },
116 | no: { // github.com/digvalley
117 | name: 'Navn',
118 | password: 'Passord',
119 | signIn: 'Logg Inn',
120 | logout: 'Logg Ut',
121 | updateAvailable: 'En ny oppdatering er tilgjengelig!',
122 | update: 'Oppdater',
123 | clients: 'Klienter',
124 | new: 'Ny',
125 | deleteClient: 'Slett Klient',
126 | deleteDialog1: 'Er du sikker på at du vil slette?',
127 | deleteDialog2: 'Denne handlingen kan ikke angres',
128 | cancel: 'Avbryt',
129 | create: 'Opprett',
130 | createdOn: 'Opprettet ',
131 | lastSeen: 'Sist sett ',
132 | totalDownload: 'Total Nedlasting: ',
133 | totalUpload: 'Total Opplasting: ',
134 | newClient: 'Ny Klient',
135 | disableClient: 'Deaktiver Klient',
136 | enableClient: 'Aktiver Klient',
137 | noClients: 'Ingen klienter opprettet enda.',
138 | showQR: 'Vis QR Kode',
139 | downloadConfig: 'Last Ned Konfigurasjon',
140 | madeBy: 'Laget av',
141 | donate: 'Doner',
142 | },
143 | pl: { // github.com/archont94
144 | name: 'Nazwa',
145 | password: 'Hasło',
146 | signIn: 'Zaloguj się',
147 | logout: 'Wyloguj się',
148 | updateAvailable: 'Dostępna aktualizacja!',
149 | update: 'Aktualizuj',
150 | clients: 'Klienci',
151 | new: 'Stwórz klienta',
152 | deleteClient: 'Usuń klienta',
153 | deleteDialog1: 'Jesteś pewny że chcesz usunąć',
154 | deleteDialog2: 'Tej akcji nie da się cofnąć.',
155 | cancel: 'Anuluj',
156 | create: 'Stwórz',
157 | createdOn: 'Utworzono ',
158 | lastSeen: 'Ostatnio widziany ',
159 | totalDownload: 'Całkowite pobieranie: ',
160 | totalUpload: 'Całkowite wysyłanie: ',
161 | newClient: 'Nowy klient',
162 | disableClient: 'Wyłączenie klienta',
163 | enableClient: 'Włączenie klienta',
164 | noClients: 'Nie ma jeszcze klientów.',
165 | showQR: 'Pokaż kod QR',
166 | downloadConfig: 'Pobierz konfigurację',
167 | madeBy: 'Stworzone przez',
168 | donate: 'Wsparcie autora',
169 | },
170 | fr: { // github.com/clem3109
171 | name: 'Nom',
172 | password: 'Mot de passe',
173 | signIn: 'Se Connecter',
174 | logout: 'Se déconnecter',
175 | updateAvailable: 'Une mise à jour est disponible !',
176 | update: 'Mise à jour',
177 | clients: 'Clients',
178 | new: 'Nouveau',
179 | deleteClient: 'Supprimer ce client',
180 | deleteDialog1: 'Êtes-vous que vous voulez supprimer',
181 | deleteDialog2: 'Cette action ne peut pas être annulée.',
182 | cancel: 'Annuler',
183 | create: 'Créer',
184 | createdOn: 'Créé le ',
185 | lastSeen: 'Dernière connexion le ',
186 | totalDownload: 'Téléchargement total : ',
187 | totalUpload: 'Téléversement total : ',
188 | newClient: 'Nouveau client',
189 | disableClient: 'Désactiver ce client',
190 | enableClient: 'Activer ce client',
191 | noClients: 'Aucun client pour le moment.',
192 | showQR: 'Afficher le code à réponse rapide (QR Code)',
193 | downloadConfig: 'Télécharger la configuration',
194 | madeBy: 'Développé par',
195 | donate: 'Soutenir',
196 | },
197 | de: { // github.com/florian-asche
198 | name: 'Name',
199 | password: 'Passwort',
200 | signIn: 'Anmelden',
201 | logout: 'Abmelden',
202 | updateAvailable: 'Eine Aktualisierung steht zur Verfügung!',
203 | update: 'Aktualisieren',
204 | clients: 'Clients',
205 | new: 'Neu',
206 | deleteClient: 'Client löschen',
207 | deleteDialog1: 'Möchtest du wirklich löschen?',
208 | deleteDialog2: 'Diese Aktion kann nicht rückgängig gemacht werden.',
209 | cancel: 'Abbrechen',
210 | create: 'Erstellen',
211 | createdOn: 'Erstellt am ',
212 | lastSeen: 'Zuletzt Online ',
213 | totalDownload: 'Gesamt Download: ',
214 | totalUpload: 'Gesamt Upload: ',
215 | newClient: 'Neuer Client',
216 | disableClient: 'Client deaktivieren',
217 | enableClient: 'Client aktivieren',
218 | noClients: 'Es wurden noch keine Clients konfiguriert.',
219 | noPrivKey: 'Es ist kein Private Key für diesen Client bekannt. Eine Konfiguration kann nicht erstellt werden.',
220 | showQR: 'Zeige den QR Code',
221 | downloadConfig: 'Konfiguration herunterladen',
222 | madeBy: 'Erstellt von',
223 | donate: 'Spenden',
224 | },
225 | ca: { // github.com/guillembonet
226 | name: 'Nom',
227 | password: 'Contrasenya',
228 | signIn: 'Iniciar sessió',
229 | logout: 'Tanca sessió',
230 | updateAvailable: 'Hi ha una actualització disponible!',
231 | update: 'Actualitza',
232 | clients: 'Clients',
233 | new: 'Nou',
234 | deleteClient: 'Esborra client',
235 | deleteDialog1: 'Estàs segur que vols esborrar aquest client?',
236 | deleteDialog2: 'Aquesta acció no es pot desfer.',
237 | cancel: 'Cancel·la',
238 | create: 'Crea',
239 | createdOn: 'Creat el ',
240 | lastSeen: 'Última connexió el ',
241 | totalDownload: 'Baixada total: ',
242 | totalUpload: 'Pujada total: ',
243 | newClient: 'Nou client',
244 | disableClient: 'Desactiva client',
245 | enableClient: 'Activa client',
246 | noClients: 'Encara no hi ha cap client.',
247 | showQR: 'Mostra codi QR',
248 | downloadConfig: 'Descarrega configuració',
249 | madeBy: 'Fet per',
250 | donate: 'Donatiu',
251 | },
252 | es: { // github.com/amarqz
253 | name: 'Nombre',
254 | password: 'Contraseña',
255 | signIn: 'Iniciar sesión',
256 | logout: 'Cerrar sesión',
257 | updateAvailable: '¡Hay una actualización disponible!',
258 | update: 'Actualizar',
259 | clients: 'Clientes',
260 | new: 'Nuevo',
261 | deleteClient: 'Eliminar cliente',
262 | deleteDialog1: '¿Estás seguro de que quieres borrar este cliente?',
263 | deleteDialog2: 'Esta acción no podrá ser revertida.',
264 | cancel: 'Cancelar',
265 | create: 'Crear',
266 | createdOn: 'Creado el ',
267 | lastSeen: 'Última conexión el ',
268 | totalDownload: 'Total descargado: ',
269 | totalUpload: 'Total subido: ',
270 | newClient: 'Nuevo cliente',
271 | disableClient: 'Desactivar cliente',
272 | enableClient: 'Activar cliente',
273 | noClients: 'Aún no hay ningún cliente.',
274 | showQR: 'Mostrar código QR',
275 | downloadConfig: 'Descargar configuración',
276 | madeBy: 'Hecho por',
277 | donate: 'Donar',
278 | toggleCharts: 'Mostrar/Ocultar gráficos',
279 | theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' },
280 | },
281 | ko: {
282 | name: '이름',
283 | password: '암호',
284 | signIn: '로그인',
285 | logout: '로그아웃',
286 | updateAvailable: '업데이트가 있습니다!',
287 | update: '업데이트',
288 | clients: '클라이언트',
289 | new: '추가',
290 | deleteClient: '클라이언트 삭제',
291 | deleteDialog1: '삭제 하시겠습니까?',
292 | deleteDialog2: '이 작업은 취소할 수 없습니다.',
293 | cancel: '취소',
294 | create: '생성',
295 | createdOn: '생성일: ',
296 | lastSeen: '마지막 사용 날짜: ',
297 | totalDownload: '총 다운로드: ',
298 | totalUpload: '총 업로드: ',
299 | newClient: '새로운 클라이언트',
300 | disableClient: '클라이언트 비활성화',
301 | enableClient: '클라이언트 활성화',
302 | noClients: '아직 클라이언트가 없습니다.',
303 | showQR: 'QR 코드 표시',
304 | downloadConfig: '구성 다운로드',
305 | madeBy: '만든 사람',
306 | donate: '기부',
307 | },
308 | vi: {
309 | name: 'Tên',
310 | password: 'Mật khẩu',
311 | signIn: 'Đăng nhập',
312 | logout: 'Đăng xuất',
313 | updateAvailable: 'Có bản cập nhật mới!',
314 | update: 'Cập nhật',
315 | clients: 'Danh sách người dùng',
316 | new: 'Mới',
317 | deleteClient: 'Xóa người dùng',
318 | deleteDialog1: 'Bạn có chắc chắn muốn xóa',
319 | deleteDialog2: 'Thao tác này không thể hoàn tác.',
320 | cancel: 'Huỷ',
321 | create: 'Tạo',
322 | createdOn: 'Được tạo lúc ',
323 | lastSeen: 'Lần xem cuối vào ',
324 | totalDownload: 'Tổng dung lượng tải xuống: ',
325 | totalUpload: 'Tổng dung lượng tải lên: ',
326 | newClient: 'Người dùng mới',
327 | disableClient: 'Vô hiệu hóa người dùng',
328 | enableClient: 'Kích hoạt người dùng',
329 | noClients: 'Hiện chưa có người dùng nào.',
330 | showQR: 'Hiển thị mã QR',
331 | downloadConfig: 'Tải xuống cấu hình',
332 | madeBy: 'Được tạo bởi',
333 | donate: 'Ủng hộ',
334 | },
335 | nl: {
336 | name: 'Naam',
337 | password: 'Wachtwoord',
338 | signIn: 'Inloggen',
339 | logout: 'Uitloggen',
340 | updateAvailable: 'Nieuw update beschikbaar!',
341 | update: 'update',
342 | clients: 'clients',
343 | new: 'Nieuw',
344 | deleteClient: 'client verwijderen',
345 | deleteDialog1: 'Weet je zeker dat je wilt verwijderen',
346 | deleteDialog2: 'Deze actie kan niet ongedaan worden gemaakt.',
347 | cancel: 'Annuleren',
348 | create: 'Creëren',
349 | createdOn: 'Gemaakt op ',
350 | lastSeen: 'Laatst gezien op ',
351 | totalDownload: 'Totaal Gedownload: ',
352 | totalUpload: 'Totaal Geupload: ',
353 | newClient: 'Nieuwe client',
354 | disableClient: 'client uitschakelen',
355 | enableClient: 'client inschakelen',
356 | noClients: 'Er zijn nog geen clients.',
357 | showQR: 'QR-code weergeven',
358 | downloadConfig: 'Configuratie downloaden',
359 | madeBy: 'Gemaakt door',
360 | donate: 'Doneren',
361 | },
362 | is: {
363 | name: 'Nafn',
364 | password: 'Lykilorð',
365 | signIn: 'Skrá inn',
366 | logout: 'Útskráning',
367 | updateAvailable: 'Það er uppfærsla í boði!',
368 | update: 'Uppfæra',
369 | clients: 'Viðskiptavinir',
370 | new: 'Nýtt',
371 | deleteClient: 'Eyða viðskiptavin',
372 | deleteDialog1: 'Ertu viss um að þú viljir eyða',
373 | deleteDialog2: 'Þessi aðgerð getur ekki verið afturkallað.',
374 | cancel: 'Hætta við',
375 | create: 'Búa til',
376 | createdOn: 'Búið til á ',
377 | lastSeen: 'Síðast séð á ',
378 | totalDownload: 'Samtals Niðurhlaða: ',
379 | totalUpload: 'Samtals Upphlaða: ',
380 | newClient: 'Nýr Viðskiptavinur',
381 | disableClient: 'Gera viðskiptavin óvirkan',
382 | enableClient: 'Gera viðskiptavin virkan',
383 | noClients: 'Engir viðskiptavinir ennþá.',
384 | showQR: 'Sýna QR-kóða',
385 | downloadConfig: 'Niðurhal Stillingar',
386 | madeBy: 'Gert af',
387 | donate: 'Gefa',
388 | },
389 | pt: {
390 | name: 'Nome',
391 | password: 'Palavra Chave',
392 | signIn: 'Entrar',
393 | logout: 'Sair',
394 | updateAvailable: 'Existe uma atualização disponível!',
395 | update: 'Atualizar',
396 | clients: 'Clientes',
397 | new: 'Novo',
398 | deleteClient: 'Apagar Clientes',
399 | deleteDialog1: 'Tem certeza que pretende apagar',
400 | deleteDialog2: 'Esta ação não pode ser revertida.',
401 | cancel: 'Cancelar',
402 | create: 'Criar',
403 | createdOn: 'Criado em ',
404 | lastSeen: 'Último acesso em ',
405 | totalDownload: 'Total Download: ',
406 | totalUpload: 'Total Upload: ',
407 | newClient: 'Novo Cliente',
408 | disableClient: 'Desativar Cliente',
409 | enableClient: 'Ativar Cliente',
410 | noClients: 'Não existem ainda clientes.',
411 | showQR: 'Apresentar o código QR',
412 | downloadConfig: 'Descarregar Configuração',
413 | madeBy: 'Feito por',
414 | donate: 'Doar',
415 | },
416 | chs: {
417 | name: '名称',
418 | password: '密码',
419 | signIn: '登录',
420 | logout: '退出',
421 | updateAvailable: '有新版本可用!',
422 | update: '更新',
423 | clients: '客户端',
424 | new: '新建',
425 | deleteClient: '删除客户端',
426 | deleteDialog1: '您确定要删除',
427 | deleteDialog2: '此操作无法撤销。',
428 | cancel: '取消',
429 | create: '创建',
430 | createdOn: '创建于 ',
431 | lastSeen: '最后访问于 ',
432 | totalDownload: '总下载: ',
433 | totalUpload: '总上传: ',
434 | newClient: '新建客户端',
435 | disableClient: '禁用客户端',
436 | enableClient: '启用客户端',
437 | noClients: '目前没有客户端。',
438 | showQR: '显示二维码',
439 | downloadConfig: '下载配置',
440 | madeBy: '由',
441 | donate: '捐赠',
442 | },
443 | cht: {
444 | name: '名字',
445 | password: '密碼',
446 | signIn: '登入',
447 | logout: '登出',
448 | updateAvailable: '有新版本可用!',
449 | update: '更新',
450 | clients: '客戶',
451 | new: '新建',
452 | deleteClient: '刪除客戶',
453 | deleteDialog1: '您確定要刪除',
454 | deleteDialog2: '此操作無法撤銷。',
455 | cancel: '取消',
456 | create: '建立',
457 | createdOn: '建立於 ',
458 | lastSeen: '最後訪問於 ',
459 | totalDownload: '總下載: ',
460 | totalUpload: '總上傳: ',
461 | newClient: '新客戶',
462 | disableClient: '禁用客戶',
463 | enableClient: '啟用客戶',
464 | noClients: '目前沒有客戶。',
465 | showQR: '顯示二維碼',
466 | downloadConfig: '下載配置',
467 | madeBy: '由',
468 | donate: '捐贈',
469 | },
470 | it: {
471 | name: 'Nome',
472 | password: 'Password',
473 | signIn: 'Accedi',
474 | logout: 'Esci',
475 | updateAvailable: 'È disponibile un aggiornamento!',
476 | update: 'Aggiorna',
477 | clients: 'Client',
478 | new: 'Nuovo',
479 | deleteClient: 'Elimina Client',
480 | deleteDialog1: 'Sei sicuro di voler eliminare',
481 | deleteDialog2: 'Questa azione non può essere annullata.',
482 | cancel: 'Annulla',
483 | create: 'Crea',
484 | createdOn: 'Creato il ',
485 | lastSeen: 'Visto l\'ultima volta il ',
486 | totalDownload: 'Totale Download: ',
487 | totalUpload: 'Totale Upload: ',
488 | newClient: 'Nuovo Client',
489 | disableClient: 'Disabilita Client',
490 | enableClient: 'Abilita Client',
491 | noClients: 'Non ci sono ancora client.',
492 | showQR: 'Mostra codice QR',
493 | downloadConfig: 'Scarica configurazione',
494 | madeBy: 'Realizzato da',
495 | donate: 'Donazione',
496 | },
497 | th: {
498 | name: 'ชื่อ',
499 | password: 'รหัสผ่าน',
500 | signIn: 'ลงชื่อเข้าใช้',
501 | logout: 'ออกจากระบบ',
502 | updateAvailable: 'มีอัปเดตพร้อมใช้งาน!',
503 | update: 'อัปเดต',
504 | clients: 'Clients',
505 | new: 'ใหม่',
506 | deleteClient: 'ลบ Client',
507 | deleteDialog1: 'คุณแน่ใจหรือไม่ว่าต้องการลบ',
508 | deleteDialog2: 'การกระทำนี้;ไม่สามารถยกเลิกได้',
509 | cancel: 'ยกเลิก',
510 | create: 'สร้าง',
511 | createdOn: 'สร้างเมื่อ ',
512 | lastSeen: 'เห็นครั้งสุดท้ายเมื่อ ',
513 | totalDownload: 'ดาวน์โหลดทั้งหมด: ',
514 | totalUpload: 'อัพโหลดทั้งหมด: ',
515 | newClient: 'Client ใหม่',
516 | disableClient: 'ปิดการใช้งาน Client',
517 | enableClient: 'เปิดการใช้งาน Client',
518 | noClients: 'ยังไม่มี Clients เลย',
519 | showQR: 'แสดงรหัส QR',
520 | downloadConfig: 'ดาวน์โหลดการตั้งค่า',
521 | madeBy: 'สร้างโดย',
522 | donate: 'บริจาค',
523 | },
524 | hi: { // github.com/rahilarious
525 | name: 'नाम',
526 | password: 'पासवर्ड',
527 | signIn: 'लॉगिन',
528 | logout: 'लॉगआउट',
529 | updateAvailable: 'अपडेट उपलब्ध है!',
530 | update: 'अपडेट',
531 | clients: 'उपयोगकर्ताये',
532 | new: 'नया',
533 | deleteClient: 'उपयोगकर्ता हटाएँ',
534 | deleteDialog1: 'क्या आपको पक्का हटाना है',
535 | deleteDialog2: 'यह निर्णय पलट नहीं सकता।',
536 | cancel: 'कुछ ना करें',
537 | create: 'बनाएं',
538 | createdOn: 'सर्जन तारीख ',
539 | lastSeen: 'पिछली बार देखे गए थे ',
540 | totalDownload: 'कुल डाउनलोड: ',
541 | totalUpload: 'कुल अपलोड: ',
542 | newClient: 'नया उपयोगकर्ता',
543 | disableClient: 'उपयोगकर्ता स्थगित कीजिये',
544 | enableClient: 'उपयोगकर्ता शुरू कीजिये',
545 | noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।',
546 | noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।',
547 | showQR: 'क्यू आर कोड देखिये',
548 | downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन',
549 | madeBy: 'सर्जक',
550 | donate: 'दान करें',
551 | },
552 | };
553 |
--------------------------------------------------------------------------------
/src/www/js/vendor/timeago.full.min.js:
--------------------------------------------------------------------------------
1 | !function(s,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((s=s||self).timeago={})}(this,function(s){"use strict";var a=["second","minute","hour","day","week","month","year"];function n(s,n){if(0===n)return["just now","right now"];var e=a[Math.floor(n/2)];return 1=m[t]&&t=m[e]&&e0;)e[n]=arguments[n+1];var r=this.$i18n;return r._t.apply(r,[t,r.locale,r._getMessages(),this].concat(e))},t.prototype.$tc=function(t,e){for(var n=[],r=arguments.length-2;r-- >0;)n[r]=arguments[r+2];var a=this.$i18n;return a._tc.apply(a,[t,a.locale,a._getMessages(),this,e].concat(n))},t.prototype.$te=function(t,e){var n=this.$i18n;return n._te(t,n.locale,n._getMessages(),e)},t.prototype.$d=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this.$i18n).d.apply(e,[t].concat(n))},t.prototype.$n=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this.$i18n).n.apply(e,[t].concat(n))}}(k),k.mixin(function(t){function e(){this!==this.$root&&this.$options.__INTLIFY_META__&&this.$el&&this.$el.setAttribute("data-intlify",this.$options.__INTLIFY_META__)}return void 0===t&&(t=!1),t?{mounted:e}:{beforeCreate:function(){var t=this.$options;if(t.i18n=t.i18n||(t.__i18nBridge||t.__i18n?{}:null),t.i18n){if(t.i18n instanceof nt){if(t.__i18nBridge||t.__i18n)try{var e=t.i18n&&t.i18n.messages?t.i18n.messages:{};(t.__i18nBridge||t.__i18n).forEach(function(t){e=g(e,JSON.parse(t))}),Object.keys(e).forEach(function(n){t.i18n.mergeLocaleMessage(n,e[n])})}catch(t){}this._i18n=t.i18n,this._i18nWatcher=this._i18n.watchI18nData()}else if(l(t.i18n)){var n=this.$root&&this.$root.$i18n&&this.$root.$i18n instanceof nt?this.$root.$i18n:null;if(n&&(t.i18n.root=this.$root,t.i18n.formatter=n.formatter,t.i18n.fallbackLocale=n.fallbackLocale,t.i18n.formatFallbackMessages=n.formatFallbackMessages,t.i18n.silentTranslationWarn=n.silentTranslationWarn,t.i18n.silentFallbackWarn=n.silentFallbackWarn,t.i18n.pluralizationRules=n.pluralizationRules,t.i18n.preserveDirectiveContent=n.preserveDirectiveContent),t.__i18nBridge||t.__i18n)try{var r=t.i18n&&t.i18n.messages?t.i18n.messages:{};(t.__i18nBridge||t.__i18n).forEach(function(t){r=g(r,JSON.parse(t))}),t.i18n.messages=r}catch(t){}var a=t.i18n.sharedMessages;a&&l(a)&&(t.i18n.messages=g(t.i18n.messages,a)),this._i18n=new nt(t.i18n),this._i18nWatcher=this._i18n.watchI18nData(),(void 0===t.i18n.sync||t.i18n.sync)&&(this._localeWatcher=this.$i18n.watchLocale()),n&&n.onComponentInstanceCreated(this._i18n)}}else this.$root&&this.$root.$i18n&&this.$root.$i18n instanceof nt?this._i18n=this.$root.$i18n:t.parent&&t.parent.$i18n&&t.parent.$i18n instanceof nt&&(this._i18n=t.parent.$i18n)},beforeMount:function(){var t=this.$options;t.i18n=t.i18n||(t.__i18nBridge||t.__i18n?{}:null),t.i18n?t.i18n instanceof nt?(this._i18n.subscribeDataChanging(this),this._subscribing=!0):l(t.i18n)&&(this._i18n.subscribeDataChanging(this),this._subscribing=!0):this.$root&&this.$root.$i18n&&this.$root.$i18n instanceof nt?(this._i18n.subscribeDataChanging(this),this._subscribing=!0):t.parent&&t.parent.$i18n&&t.parent.$i18n instanceof nt&&(this._i18n.subscribeDataChanging(this),this._subscribing=!0)},mounted:e,beforeDestroy:function(){if(this._i18n){var t=this;this.$nextTick(function(){t._subscribing&&(t._i18n.unsubscribeDataChanging(t),delete t._subscribing),t._i18nWatcher&&(t._i18nWatcher(),t._i18n.destroyVM(),delete t._i18nWatcher),t._localeWatcher&&(t._localeWatcher(),delete t._localeWatcher)})}}}}(e.bridge)),k.directive("t",{bind:$,update:M,unbind:T}),k.component(d.name,d),k.component(w.name,w),k.config.optionMergeStrategies.i18n=function(t,e){return void 0===e?t:e}}var O=function(){this._caches=Object.create(null)};O.prototype.interpolate=function(t,e){if(!e)return[t];var n=this._caches[t];return n||(n=function(t){var e=[],n=0,r="";for(;n0)h--,u=V,f[j]();else{if(h=0,void 0===n)return!1;if(!1===(n=q(n)))return!1;f[N]()}};null!==u;)if("\\"!==(e=t[++c])||!p()){if(a=G(e),(i=(s=U[u])[a]||s.else||B)===B)return;if(u=i[0],(o=f[i[1]])&&(r=void 0===(r=i[2])?e:r,!1===o()))return;if(u===A)return l}}(t))&&(this._cache[t]=e),e||[]},J.prototype.getPathValue=function(t,e){if(!a(t))return null;var n=this.parsePath(e);if(0===n.length)return null;for(var r=n.length,i=t,o=0;o/,X=/(?:@(?:\.[a-zA-Z]+)?:(?:[\w\-_|./]+|\([\w\-_:|./]+\)))/g,K=/^@(?:\.([a-zA-Z]+))?:/,Q=/[()]/g,tt={upper:function(t){return t.toLocaleUpperCase()},lower:function(t){return t.toLocaleLowerCase()},capitalize:function(t){return""+t.charAt(0).toLocaleUpperCase()+t.substr(1)}},et=new O,nt=function(t){var e=this;void 0===t&&(t={}),!k&&"undefined"!=typeof window&&window.Vue&&L(window.Vue);var n=t.locale||"en-US",r=!1!==t.fallbackLocale&&(t.fallbackLocale||"en-US"),a=t.messages||{},i=t.dateTimeFormats||t.datetimeFormats||{},o=t.numberFormats||{};this._vm=null,this._formatter=t.formatter||et,this._modifiers=t.modifiers||{},this._missing=t.missing||null,this._root=t.root||null,this._sync=void 0===t.sync||!!t.sync,this._fallbackRoot=void 0===t.fallbackRoot||!!t.fallbackRoot,this._fallbackRootWithEmptyString=void 0===t.fallbackRootWithEmptyString||!!t.fallbackRootWithEmptyString,this._formatFallbackMessages=void 0!==t.formatFallbackMessages&&!!t.formatFallbackMessages,this._silentTranslationWarn=void 0!==t.silentTranslationWarn&&t.silentTranslationWarn,this._silentFallbackWarn=void 0!==t.silentFallbackWarn&&!!t.silentFallbackWarn,this._dateTimeFormatters={},this._numberFormatters={},this._path=new J,this._dataListeners=new Set,this._componentInstanceCreatedListener=t.componentInstanceCreatedListener||null,this._preserveDirectiveContent=void 0!==t.preserveDirectiveContent&&!!t.preserveDirectiveContent,this.pluralizationRules=t.pluralizationRules||{},this._warnHtmlInMessage=t.warnHtmlInMessage||"off",this._postTranslation=t.postTranslation||null,this._escapeParameterHtml=t.escapeParameterHtml||!1,"__VUE_I18N_BRIDGE__"in t&&(this.__VUE_I18N_BRIDGE__=t.__VUE_I18N_BRIDGE__),this.getChoiceIndex=function(t,n){var r=Object.getPrototypeOf(e);if(r&&r.getChoiceIndex)return r.getChoiceIndex.call(e,t,n);var a,i;return e.locale in e.pluralizationRules?e.pluralizationRules[e.locale].apply(e,[t,n]):(a=t,i=n,a=Math.abs(a),2===i?a?a>1?1:0:1:a?Math.min(a,2):0)},this._exist=function(t,n){return!(!t||!n)&&(!c(e._path.getPathValue(t,n))||!!t[n])},"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||Object.keys(a).forEach(function(t){e._checkLocaleMessage(t,e._warnHtmlInMessage,a[t])}),this._initVM({locale:n,fallbackLocale:r,messages:a,dateTimeFormats:i,numberFormats:o})},rt={vm:{configurable:!0},messages:{configurable:!0},dateTimeFormats:{configurable:!0},numberFormats:{configurable:!0},availableLocales:{configurable:!0},locale:{configurable:!0},fallbackLocale:{configurable:!0},formatFallbackMessages:{configurable:!0},missing:{configurable:!0},formatter:{configurable:!0},silentTranslationWarn:{configurable:!0},silentFallbackWarn:{configurable:!0},preserveDirectiveContent:{configurable:!0},warnHtmlInMessage:{configurable:!0},postTranslation:{configurable:!0},sync:{configurable:!0}};return nt.prototype._checkLocaleMessage=function(t,e,a){var o=function(t,e,a,s){if(l(a))Object.keys(a).forEach(function(n){var r=a[n];l(r)?(s.push(n),s.push("."),o(t,e,r,s),s.pop(),s.pop()):(s.push(n),o(t,e,r,s),s.pop())});else if(r(a))a.forEach(function(n,r){l(n)?(s.push("["+r+"]"),s.push("."),o(t,e,n,s),s.pop(),s.pop()):(s.push("["+r+"]"),o(t,e,n,s),s.pop())});else if(i(a)){if(Y.test(a)){var c="Detected HTML in message '"+a+"' of keypath '"+s.join("")+"' at '"+e+"'. Consider component interpolation with '' to avoid XSS. See https://bit.ly/2ZqJzkp";"warn"===t?n(c):"error"===t&&function(t,e){"undefined"!=typeof console&&(console.error("[vue-i18n] "+t),e&&console.error(e.stack))}(c)}}};o(e,t,a,[])},nt.prototype._initVM=function(t){var e=k.config.silent;k.config.silent=!0,this._vm=new k({data:t,__VUE18N__INSTANCE__:!0}),k.config.silent=e},nt.prototype.destroyVM=function(){this._vm.$destroy()},nt.prototype.subscribeDataChanging=function(t){this._dataListeners.add(t)},nt.prototype.unsubscribeDataChanging=function(t){!function(t,e){if(t.delete(e));}(this._dataListeners,t)},nt.prototype.watchI18nData=function(){var t=this;return this._vm.$watch("$data",function(){for(var e,n,r=(e=t._dataListeners,n=[],e.forEach(function(t){return n.push(t)}),n),a=r.length;a--;)k.nextTick(function(){r[a]&&r[a].$forceUpdate()})},{deep:!0})},nt.prototype.watchLocale=function(t){if(t){if(!this.__VUE_I18N_BRIDGE__)return null;var e=this,n=this._vm;return this.vm.$watch("locale",function(r){n.$set(n,"locale",r),e.__VUE_I18N_BRIDGE__&&t&&(t.locale.value=r),n.$forceUpdate()},{immediate:!0})}if(!this._sync||!this._root)return null;var r=this._vm;return this._root.$i18n.vm.$watch("locale",function(t){r.$set(r,"locale",t),r.$forceUpdate()},{immediate:!0})},nt.prototype.onComponentInstanceCreated=function(t){this._componentInstanceCreatedListener&&this._componentInstanceCreatedListener(t,this)},rt.vm.get=function(){return this._vm},rt.messages.get=function(){return f(this._getMessages())},rt.dateTimeFormats.get=function(){return f(this._getDateTimeFormats())},rt.numberFormats.get=function(){return f(this._getNumberFormats())},rt.availableLocales.get=function(){return Object.keys(this.messages).sort()},rt.locale.get=function(){return this._vm.locale},rt.locale.set=function(t){this._vm.$set(this._vm,"locale",t)},rt.fallbackLocale.get=function(){return this._vm.fallbackLocale},rt.fallbackLocale.set=function(t){this._localeChainCache={},this._vm.$set(this._vm,"fallbackLocale",t)},rt.formatFallbackMessages.get=function(){return this._formatFallbackMessages},rt.formatFallbackMessages.set=function(t){this._formatFallbackMessages=t},rt.missing.get=function(){return this._missing},rt.missing.set=function(t){this._missing=t},rt.formatter.get=function(){return this._formatter},rt.formatter.set=function(t){this._formatter=t},rt.silentTranslationWarn.get=function(){return this._silentTranslationWarn},rt.silentTranslationWarn.set=function(t){this._silentTranslationWarn=t},rt.silentFallbackWarn.get=function(){return this._silentFallbackWarn},rt.silentFallbackWarn.set=function(t){this._silentFallbackWarn=t},rt.preserveDirectiveContent.get=function(){return this._preserveDirectiveContent},rt.preserveDirectiveContent.set=function(t){this._preserveDirectiveContent=t},rt.warnHtmlInMessage.get=function(){return this._warnHtmlInMessage},rt.warnHtmlInMessage.set=function(t){var e=this,n=this._warnHtmlInMessage;if(this._warnHtmlInMessage=t,n!==t&&("warn"===t||"error"===t)){var r=this._getMessages();Object.keys(r).forEach(function(t){e._checkLocaleMessage(t,e._warnHtmlInMessage,r[t])})}},rt.postTranslation.get=function(){return this._postTranslation},rt.postTranslation.set=function(t){this._postTranslation=t},rt.sync.get=function(){return this._sync},rt.sync.set=function(t){this._sync=t},nt.prototype._getMessages=function(){return this._vm.messages},nt.prototype._getDateTimeFormats=function(){return this._vm.dateTimeFormats},nt.prototype._getNumberFormats=function(){return this._vm.numberFormats},nt.prototype._warnDefault=function(t,e,n,r,a,o){if(!c(n))return n;if(this._missing){var s=this._missing.apply(null,[t,e,r,a]);if(i(s))return s}if(this._formatFallbackMessages){var l=h.apply(void 0,a);return this._render(e,o,l.params,e)}return e},nt.prototype._isFallbackRoot=function(t){return(this._fallbackRootWithEmptyString?!t:c(t))&&!c(this._root)&&this._fallbackRoot},nt.prototype._isSilentFallbackWarn=function(t){return this._silentFallbackWarn instanceof RegExp?this._silentFallbackWarn.test(t):this._silentFallbackWarn},nt.prototype._isSilentFallback=function(t,e){return this._isSilentFallbackWarn(e)&&(this._isFallbackRoot()||t!==this.fallbackLocale)},nt.prototype._isSilentTranslationWarn=function(t){return this._silentTranslationWarn instanceof RegExp?this._silentTranslationWarn.test(t):this._silentTranslationWarn},nt.prototype._interpolate=function(t,e,n,a,o,s,h){if(!e)return null;var f,p=this._path.getPathValue(e,n);if(r(p)||l(p))return p;if(c(p)){if(!l(e))return null;if(!i(f=e[n])&&!u(f))return null}else{if(!i(p)&&!u(p))return null;f=p}return i(f)&&(f.indexOf("@:")>=0||f.indexOf("@.")>=0)&&(f=this._link(t,e,f,a,"raw",s,h)),this._render(f,o,s,n)},nt.prototype._link=function(t,e,n,a,i,o,s){var l=n,c=l.match(X);for(var u in c)if(c.hasOwnProperty(u)){var h=c[u],f=h.match(K),_=f[0],m=f[1],g=h.replace(_,"").replace(Q,"");if(p(s,g))return l;s.push(g);var v=this._interpolate(t,e,g,a,"raw"===i?"string":i,"raw"===i?void 0:o,s);if(this._isFallbackRoot(v)){if(!this._root)throw Error("unexpected error");var d=this._root.$i18n;v=d._translate(d._getMessages(),d.locale,d.fallbackLocale,g,a,i,o)}v=this._warnDefault(t,g,v,a,r(o)?o:[o],i),this._modifiers.hasOwnProperty(m)?v=this._modifiers[m](v):tt.hasOwnProperty(m)&&(v=tt[m](v)),s.pop(),l=v?l.replace(h,v):l}return l},nt.prototype._createMessageContext=function(t,e,n,i){var o=this,s=r(t)?t:[],l=a(t)?t:{},c=this._getMessages(),u=this.locale;return{list:function(t){return s[t]},named:function(t){return l[t]},values:t,formatter:e,path:n,messages:c,locale:u,linked:function(t){return o._interpolate(u,c[u]||{},t,null,i,void 0,[t])}}},nt.prototype._render=function(t,e,n,r){if(u(t))return t(this._createMessageContext(n,this._formatter||et,r,e));var a=this._formatter.interpolate(t,n,r);return a||(a=et.interpolate(t,n,r)),"string"!==e||i(a)?a:a.join("")},nt.prototype._appendItemToChain=function(t,e,n){var r=!1;return p(t,e)||(r=!0,e&&(r="!"!==e[e.length-1],e=e.replace(/!/g,""),t.push(e),n&&n[e]&&(r=n[e]))),r},nt.prototype._appendLocaleToChain=function(t,e,n){var r,a=e.split("-");do{var i=a.join("-");r=this._appendItemToChain(t,i,n),a.splice(-1,1)}while(a.length&&!0===r);return r},nt.prototype._appendBlockToChain=function(t,e,n){for(var r=!0,a=0;a0;)i[o]=arguments[o+4];if(!t)return"";var s,l=h.apply(void 0,i);this._escapeParameterHtml&&(l.params=(null!=(s=l.params)&&Object.keys(s).forEach(function(t){"string"==typeof s[t]&&(s[t]=s[t].replace(//g,">").replace(/"/g,""").replace(/'/g,"'"))}),s));var c=l.locale||e,u=this._translate(n,c,this.fallbackLocale,t,r,"string",l.params);if(this._isFallbackRoot(u)){if(!this._root)throw Error("unexpected error");return(a=this._root).$t.apply(a,[t].concat(i))}return u=this._warnDefault(c,t,u,r,i,"string"),this._postTranslation&&null!=u&&(u=this._postTranslation(u,t)),u},nt.prototype.t=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this)._t.apply(e,[t,this.locale,this._getMessages(),null].concat(n))},nt.prototype._i=function(t,e,n,r,a){var i=this._translate(n,e,this.fallbackLocale,t,r,"raw",a);if(this._isFallbackRoot(i)){if(!this._root)throw Error("unexpected error");return this._root.$i18n.i(t,e,a)}return this._warnDefault(e,t,i,r,[a],"raw")},nt.prototype.i=function(t,e,n){return t?(i(e)||(e=this.locale),this._i(t,e,this._getMessages(),null,n)):""},nt.prototype._tc=function(t,e,n,r,a){for(var i,o=[],s=arguments.length-5;s-- >0;)o[s]=arguments[s+5];if(!t)return"";void 0===a&&(a=1);var l={count:a,n:a},c=h.apply(void 0,o);return c.params=Object.assign(l,c.params),o=null===c.locale?[c.params]:[c.locale,c.params],this.fetchChoice((i=this)._t.apply(i,[t,e,n,r].concat(o)),a)},nt.prototype.fetchChoice=function(t,e){if(!t||!i(t))return null;var n=t.split("|");return n[e=this.getChoiceIndex(e,n.length)]?n[e].trim():t},nt.prototype.tc=function(t,e){for(var n,r=[],a=arguments.length-2;a-- >0;)r[a]=arguments[a+2];return(n=this)._tc.apply(n,[t,this.locale,this._getMessages(),null,e].concat(r))},nt.prototype._te=function(t,e,n){for(var r=[],a=arguments.length-3;a-- >0;)r[a]=arguments[a+3];var i=h.apply(void 0,r).locale||e;return this._exist(n[i],t)},nt.prototype.te=function(t,e){return this._te(t,this.locale,this._getMessages(),e)},nt.prototype.getLocaleMessage=function(t){return f(this._vm.messages[t]||{})},nt.prototype.setLocaleMessage=function(t,e){"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||this._checkLocaleMessage(t,this._warnHtmlInMessage,e),this._vm.$set(this._vm.messages,t,e)},nt.prototype.mergeLocaleMessage=function(t,e){"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||this._checkLocaleMessage(t,this._warnHtmlInMessage,e),this._vm.$set(this._vm.messages,t,g(void 0!==this._vm.messages[t]&&Object.keys(this._vm.messages[t]).length?Object.assign({},this._vm.messages[t]):{},e))},nt.prototype.getDateTimeFormat=function(t){return f(this._vm.dateTimeFormats[t]||{})},nt.prototype.setDateTimeFormat=function(t,e){this._vm.$set(this._vm.dateTimeFormats,t,e),this._clearDateTimeFormat(t,e)},nt.prototype.mergeDateTimeFormat=function(t,e){this._vm.$set(this._vm.dateTimeFormats,t,g(this._vm.dateTimeFormats[t]||{},e)),this._clearDateTimeFormat(t,e)},nt.prototype._clearDateTimeFormat=function(t,e){for(var n in e){var r=t+"__"+n;this._dateTimeFormatters.hasOwnProperty(r)&&delete this._dateTimeFormatters[r]}},nt.prototype._localizeDateTime=function(t,e,n,r,a,i){for(var o=e,s=r[o],l=this._getLocaleChain(e,n),u=0;u0;)n[r]=arguments[r+1];var o=this.locale,s=null,l=null;return 1===n.length?(i(n[0])?s=n[0]:a(n[0])&&(n[0].locale&&(o=n[0].locale),n[0].key&&(s=n[0].key)),l=Object.keys(n[0]).reduce(function(t,r){var a;return p(e,r)?Object.assign({},t,((a={})[r]=n[0][r],a)):t},null)):2===n.length&&(i(n[0])&&(s=n[0]),i(n[1])&&(o=n[1])),this._d(t,o,s,l)},nt.prototype.getNumberFormat=function(t){return f(this._vm.numberFormats[t]||{})},nt.prototype.setNumberFormat=function(t,e){this._vm.$set(this._vm.numberFormats,t,e),this._clearNumberFormat(t,e)},nt.prototype.mergeNumberFormat=function(t,e){this._vm.$set(this._vm.numberFormats,t,g(this._vm.numberFormats[t]||{},e)),this._clearNumberFormat(t,e)},nt.prototype._clearNumberFormat=function(t,e){for(var n in e){var r=t+"__"+n;this._numberFormatters.hasOwnProperty(r)&&delete this._numberFormatters[r]}},nt.prototype._getNumberFormatter=function(t,e,n,r,a,i){for(var o=e,s=r[o],l=this._getLocaleChain(e,n),u=0;u0;)n[r]=arguments[r+1];var o=this.locale,s=null,l=null;return 1===n.length?i(n[0])?s=n[0]:a(n[0])&&(n[0].locale&&(o=n[0].locale),n[0].key&&(s=n[0].key),l=Object.keys(n[0]).reduce(function(e,r){var a;return p(t,r)?Object.assign({},e,((a={})[r]=n[0][r],a)):e},null)):2===n.length&&(i(n[0])&&(s=n[0]),i(n[1])&&(o=n[1])),this._n(e,o,s,l)},nt.prototype._ntp=function(t,e,n,r){if(!nt.availabilities.numberFormat)return[];if(!n)return(r?new Intl.NumberFormat(e,r):new Intl.NumberFormat(e)).formatToParts(t);var a=this._getNumberFormatter(t,e,this.fallbackLocale,this._getNumberFormats(),n,r),i=a&&a.formatToParts(t);if(this._isFallbackRoot(i)){if(!this._root)throw Error("unexpected error");return this._root.$i18n._ntp(t,e,n,r)}return i||[]},Object.defineProperties(nt.prototype,rt),Object.defineProperty(nt,"availabilities",{get:function(){if(!Z){var t="undefined"!=typeof Intl;Z={dateTimeFormat:t&&void 0!==Intl.DateTimeFormat,numberFormat:t&&void 0!==Intl.NumberFormat}}return Z}}),nt.install=L,nt.version="8.28.2",nt},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueI18n=e();
7 |
--------------------------------------------------------------------------------
/src/www/img/logo.svg:
--------------------------------------------------------------------------------
1 |
34 |
--------------------------------------------------------------------------------