├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------