forked from mirrors/amnezia-wg-easy
		
	wip
This commit is contained in:
		
							parent
							
								
									eaf3d5c3fb
								
							
						
					
					
						commit
						d7bb645470
					
				
					 25 changed files with 4632 additions and 2 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								assets/wg-easy.sketch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/wg-easy.sketch
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										11
									
								
								config/wg0 copy.conf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								config/wg0 copy.conf
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
[Interface]
 | 
			
		||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o= # wg genkey > privatekey
 | 
			
		||||
# PublicKey = BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0=
 | 
			
		||||
Address = 10.8.0.1/24
 | 
			
		||||
ListenPort = 51820
 | 
			
		||||
DNS = 1.1.1.1
 | 
			
		||||
 | 
			
		||||
[Peer]
 | 
			
		||||
PublicKey = i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4=
 | 
			
		||||
PresharedKey = MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU=
 | 
			
		||||
AllowedIPs = 10.8.0.2/32
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,15 @@
 | 
			
		|||
 | 
			
		||||
# Note: Do not edit this file directly.
 | 
			
		||||
# Your changes will be overwritten!
 | 
			
		||||
 | 
			
		||||
# Server
 | 
			
		||||
[Interface]
 | 
			
		||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o= # wg genkey > privatekey
 | 
			
		||||
# PublicKey = BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0=
 | 
			
		||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o=
 | 
			
		||||
Address = 10.8.0.1/24
 | 
			
		||||
ListenPort = 51820
 | 
			
		||||
DNS = 1.1.1.1
 | 
			
		||||
 | 
			
		||||
# Client: Emile (af3111a4-7343-4380-a293-ed498d9aa3b8)
 | 
			
		||||
[Peer]
 | 
			
		||||
PublicKey = i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4=
 | 
			
		||||
PresharedKey = MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								config/wg0.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								config/wg0.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
{
 | 
			
		||||
  "server": {
 | 
			
		||||
    "privateKey": "iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o=",
 | 
			
		||||
    "publicKey": "BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0=",
 | 
			
		||||
    "address": "10.8.0.1/24",
 | 
			
		||||
    "port": "51820",
 | 
			
		||||
    "dns": "1.1.1.1"
 | 
			
		||||
  },
 | 
			
		||||
  "clients": {
 | 
			
		||||
    "af3111a4-7343-4380-a293-ed498d9aa3b8": {
 | 
			
		||||
      "name": "Emile",
 | 
			
		||||
      "createdAt": "2021-05-22T20:02:45.372Z",
 | 
			
		||||
      "updatedAt": "2021-05-22T20:02:45.372Z",
 | 
			
		||||
      "privateKey": "sHUUDbaZBQshfOvvF8HeebhhXq3rDKWlW1Vm+6XMklU=",
 | 
			
		||||
      "publicKey": "i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4=",
 | 
			
		||||
      "preSharedKey": "MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU=",
 | 
			
		||||
      "address": "10.8.0.2/32",
 | 
			
		||||
      "allowedIPs": "10.8.0.2/32",
 | 
			
		||||
      "enabled": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/.eslintrc.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/.eslintrc.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
  "extends": "athom",
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "consistent-return": "off",
 | 
			
		||||
    "no-shadow": "off"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/node_modules
 | 
			
		||||
							
								
								
									
										8
									
								
								src/config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports.PORT = process.env.PORT || 80;
 | 
			
		||||
module.exports.PASSWORD = process.env.PASSWORD || 'wireguard';
 | 
			
		||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
 | 
			
		||||
module.exports.WG_HOST = process.env.WG_HOST || '127.0.0.1';
 | 
			
		||||
module.exports.WG_PORT = process.env.WG_PORT || 51820;
 | 
			
		||||
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.6.0.1/32';
 | 
			
		||||
							
								
								
									
										104
									
								
								src/lib/Server.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/lib/Server.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,104 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
const express = require('express');
 | 
			
		||||
const expressSession = require('express-session');
 | 
			
		||||
const debug = require('debug')('Server');
 | 
			
		||||
 | 
			
		||||
const Util = require('./Util');
 | 
			
		||||
const ServerError = require('./ServerError');
 | 
			
		||||
const WireGuard = require('../services/WireGuard');
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  PORT,
 | 
			
		||||
  PASSWORD,
 | 
			
		||||
} = require('../config');
 | 
			
		||||
 | 
			
		||||
module.exports = class Server {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    // Express
 | 
			
		||||
    this.app = express()
 | 
			
		||||
      .use('/', express.static(path.join(__dirname, '..', 'www')))
 | 
			
		||||
      .use(express.json())
 | 
			
		||||
      .use(expressSession({
 | 
			
		||||
        secret: String(Math.random()),
 | 
			
		||||
        resave: true,
 | 
			
		||||
        saveUninitialized: true,
 | 
			
		||||
      }))
 | 
			
		||||
 | 
			
		||||
      // Authentication
 | 
			
		||||
      .get('/api/session', Util.promisify(async req => {
 | 
			
		||||
        return {
 | 
			
		||||
          authenticated: !!(req.session && req.session.authenticated),
 | 
			
		||||
        };
 | 
			
		||||
      }))
 | 
			
		||||
      .post('/api/session', Util.promisify(async req => {
 | 
			
		||||
        const {
 | 
			
		||||
          password,
 | 
			
		||||
        } = req.body;
 | 
			
		||||
 | 
			
		||||
        if (typeof password !== 'string') {
 | 
			
		||||
          throw new ServerError('Missing: Password', 401);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (password !== PASSWORD) {
 | 
			
		||||
          throw new ServerError('Incorrect Password', 401);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        req.session.authenticated = true;
 | 
			
		||||
        req.session.save();
 | 
			
		||||
 | 
			
		||||
        debug(`New Session: ${req.session.id})`);
 | 
			
		||||
      }))
 | 
			
		||||
 | 
			
		||||
      // WireGuard
 | 
			
		||||
      .use(Util.requireSession)
 | 
			
		||||
      .delete('/api/session', Util.promisify(async req => {
 | 
			
		||||
        const sessionId = req.session.id;
 | 
			
		||||
 | 
			
		||||
        req.session.destroy();
 | 
			
		||||
 | 
			
		||||
        debug(`Deleted Session: ${sessionId}`);
 | 
			
		||||
      }))
 | 
			
		||||
      .get('/api/wireguard/client', Util.promisify(async req => {
 | 
			
		||||
        return WireGuard.getClients();
 | 
			
		||||
      }))
 | 
			
		||||
      .get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => {
 | 
			
		||||
        const { clientId } = req.params;
 | 
			
		||||
        const svg = await WireGuard.getClientQRCodeSVG({ clientId });
 | 
			
		||||
        res.header('Content-Type', 'image/svg+xml');
 | 
			
		||||
        res.send(svg);
 | 
			
		||||
      }))
 | 
			
		||||
      .get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => {
 | 
			
		||||
        const { clientId } = req.params;
 | 
			
		||||
        const client = await WireGuard.getClient({ clientId });
 | 
			
		||||
        const config = await WireGuard.getClientConfiguration({ clientId });
 | 
			
		||||
        res.header('Content-Disposition', `attachment; filename="${client.name}.conf"`);
 | 
			
		||||
        res.header('Content-Type', 'text/plain');
 | 
			
		||||
        res.send(config);
 | 
			
		||||
      }))
 | 
			
		||||
      .post('/api/wireguard/client', Util.promisify(async req => {
 | 
			
		||||
        const { name } = req.body;
 | 
			
		||||
        return WireGuard.createClient({ name });
 | 
			
		||||
      }))
 | 
			
		||||
      .delete('/api/wireguard/client/:clientId', Util.promisify(async req => {
 | 
			
		||||
        const { clientId } = req.params;
 | 
			
		||||
        return WireGuard.deleteClient({ clientId });
 | 
			
		||||
      }))
 | 
			
		||||
      .post('/api/wireguard/client/:clientId/enable', Util.promisify(async req => {
 | 
			
		||||
        const { clientId } = req.params;
 | 
			
		||||
        return WireGuard.enableClient({ clientId });
 | 
			
		||||
      }))
 | 
			
		||||
      .post('/api/wireguard/client/:clientId/disable', Util.promisify(async req => {
 | 
			
		||||
        const { clientId } = req.params;
 | 
			
		||||
        return WireGuard.disableClient({ clientId });
 | 
			
		||||
      }))
 | 
			
		||||
 | 
			
		||||
      .listen(PORT, () => {
 | 
			
		||||
        debug(`Listening on http://0.0.0.0:${PORT}`);
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										10
									
								
								src/lib/ServerError.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/lib/ServerError.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports = class ServerError extends Error {
 | 
			
		||||
 | 
			
		||||
  constructor(message, statusCode = 500) {
 | 
			
		||||
    super(message);
 | 
			
		||||
    this.statusCode = statusCode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										46
									
								
								src/lib/Util.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/lib/Util.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports = class Util {
 | 
			
		||||
 | 
			
		||||
  static requireSession(req, res, next) {
 | 
			
		||||
    if (req.session && req.session.authenticated) {
 | 
			
		||||
      return next();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return res.status(401).json({
 | 
			
		||||
      error: 'Not Logged In',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static promisify(fn) {
 | 
			
		||||
    return function(req, res) {
 | 
			
		||||
      Promise.resolve().then(async () => fn(req, res))
 | 
			
		||||
        .then(result => {
 | 
			
		||||
          if (res.headersSent) return;
 | 
			
		||||
 | 
			
		||||
          if (typeof result === 'undefined') {
 | 
			
		||||
            return res
 | 
			
		||||
              .status(204)
 | 
			
		||||
              .end();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return res
 | 
			
		||||
            .status(200)
 | 
			
		||||
            .json(result);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
          if (typeof error === 'string') {
 | 
			
		||||
            error = new Error(error);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return res
 | 
			
		||||
            .status(error.statusCode || 500)
 | 
			
		||||
            .json({
 | 
			
		||||
              error: error.message || error.toString(),
 | 
			
		||||
              stack: error.stack,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										183
									
								
								src/lib/WireGuard.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/lib/WireGuard.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,183 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
const fs = require('fs').promises;
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
const QRCode = require('qrcode');
 | 
			
		||||
 | 
			
		||||
const ServerError = require('./ServerError');
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  WG_PATH,
 | 
			
		||||
  WG_HOST,
 | 
			
		||||
  WG_PORT,
 | 
			
		||||
  WG_DEFAULT_ADDRESS,
 | 
			
		||||
} = require('../config');
 | 
			
		||||
 | 
			
		||||
module.exports = class WireGuard {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    Promise.resolve().then(async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
 | 
			
		||||
        this.config = JSON.parse(config);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        this.config = {
 | 
			
		||||
          // TODO: Generate new config
 | 
			
		||||
          server: {
 | 
			
		||||
            address: WG_DEFAULT_ADDRESS,
 | 
			
		||||
          },
 | 
			
		||||
          clients: {},
 | 
			
		||||
        };
 | 
			
		||||
        // TODO: Save JSON config
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log('this.config', this.config);
 | 
			
		||||
 | 
			
		||||
      await this.saveConfig();
 | 
			
		||||
    }).catch(err => {
 | 
			
		||||
      // eslint-disable-next-line no-console
 | 
			
		||||
      console.error(err);
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line no-process-exit
 | 
			
		||||
      process.exit(1);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async saveConfig() {
 | 
			
		||||
    let result = `
 | 
			
		||||
# Note: Do not edit this file directly.
 | 
			
		||||
# Your changes will be overwritten!
 | 
			
		||||
 | 
			
		||||
# Server
 | 
			
		||||
[Interface]
 | 
			
		||||
PrivateKey = ${this.config.server.privateKey}
 | 
			
		||||
Address = ${this.config.server.address}
 | 
			
		||||
ListenPort = ${this.config.server.port}
 | 
			
		||||
DNS = ${this.config.server.dns}`;
 | 
			
		||||
 | 
			
		||||
    for (const [clientId, client] of Object.entries(this.config.clients)) {
 | 
			
		||||
      if (!client.enabled) continue;
 | 
			
		||||
 | 
			
		||||
      result += `
 | 
			
		||||
 | 
			
		||||
# Client: ${client.name} (${clientId})
 | 
			
		||||
[Peer]
 | 
			
		||||
PublicKey = ${client.publicKey}
 | 
			
		||||
PresharedKey = ${client.preSharedKey}
 | 
			
		||||
AllowedIPs = ${client.allowedIPs}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(this.config, false, 2));
 | 
			
		||||
    await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getClients() {
 | 
			
		||||
    return Object.entries(this.config.clients).map(([clientId, client]) => ({
 | 
			
		||||
      id: clientId,
 | 
			
		||||
      name: client.name,
 | 
			
		||||
      enabled: client.enabled,
 | 
			
		||||
      publicKey: client.publicKey,
 | 
			
		||||
      createdAt: client.createdAt,
 | 
			
		||||
      updatedAt: client.updatedAt,
 | 
			
		||||
      allowedIPs: client.allowedIPs,
 | 
			
		||||
 | 
			
		||||
      // TODO:
 | 
			
		||||
      latestHandshake: new Date(),
 | 
			
		||||
      transferRx: 0,
 | 
			
		||||
      transferTx: 0,
 | 
			
		||||
    }));
 | 
			
		||||
    // const { stdout } = await Util.exec('sudo cat /etc/wireguard/configs/clients.txt');
 | 
			
		||||
    // return stdout
 | 
			
		||||
    //   .trim()
 | 
			
		||||
    //   .split('\n')
 | 
			
		||||
    //   .filter(line => {
 | 
			
		||||
    //     return line.length > 0;
 | 
			
		||||
    //   })
 | 
			
		||||
    //   .map(line => {
 | 
			
		||||
    //     const [ name, publicKey, createdAt ] = line.split(' ');
 | 
			
		||||
    //     return {
 | 
			
		||||
    //       name,
 | 
			
		||||
    //       publicKey,
 | 
			
		||||
    //       createdAt: new Date(Number(createdAt + '000')),
 | 
			
		||||
    //     };
 | 
			
		||||
    //   });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getClient({ clientId }) {
 | 
			
		||||
    const client = this.config.clients[clientId];
 | 
			
		||||
    if (!client) {
 | 
			
		||||
      throw new ServerError(`Client Not Found: ${clientId}`, 404);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return client;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getClientConfiguration({ clientId }) {
 | 
			
		||||
    const client = await this.getClient({ clientId });
 | 
			
		||||
 | 
			
		||||
    return `
 | 
			
		||||
[Interface]
 | 
			
		||||
PrivateKey = ${client.privateKey}
 | 
			
		||||
Address = ${client.address}
 | 
			
		||||
DNS = ${this.config.server.dns}
 | 
			
		||||
 | 
			
		||||
[Peer]
 | 
			
		||||
PublicKey = ${client.publicKey}
 | 
			
		||||
PresharedKey = ${client.preSharedKey}
 | 
			
		||||
AllowedIPs = ${client.allowedIPs}
 | 
			
		||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getClientQRCodeSVG({ clientId }) {
 | 
			
		||||
    const config = await this.getClientConfiguration({ clientId });
 | 
			
		||||
    return QRCode.toString(config, {
 | 
			
		||||
      type: 'svg',
 | 
			
		||||
      width: 512,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createClient({ name }) {
 | 
			
		||||
    if (!name) {
 | 
			
		||||
      throw new Error('Missing: Name');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // try {
 | 
			
		||||
    //   await this.getClient({ name });
 | 
			
		||||
    //   throw new Error(`Duplicate Client: ${name}`);
 | 
			
		||||
    // } catch( err ) {
 | 
			
		||||
    //   if( err.message.startsWith('Duplicate Client') ) {
 | 
			
		||||
    //     throw err;
 | 
			
		||||
    //   }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // // TODO: This is unsafe
 | 
			
		||||
    // await this.ssh.exec(`pivpn add -n ${name}`);
 | 
			
		||||
 | 
			
		||||
    // return this.getClient({ name });
 | 
			
		||||
 | 
			
		||||
    await this.saveConfig();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deleteClient({ clientId }) {
 | 
			
		||||
    if (this.config.clients[clientId]) {
 | 
			
		||||
      delete this.config.clients[clientId];
 | 
			
		||||
      await this.saveConfig();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async enableClient({ clientId }) {
 | 
			
		||||
    const client = await this.getClient({ clientId });
 | 
			
		||||
    client.enabled = true;
 | 
			
		||||
 | 
			
		||||
    await this.saveConfig();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async disableClient({ clientId }) {
 | 
			
		||||
    const client = await this.getClient({ clientId });
 | 
			
		||||
    client.enabled = false;
 | 
			
		||||
 | 
			
		||||
    await this.saveConfig();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										3560
									
								
								src/package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3560
									
								
								src/package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										31
									
								
								src/package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/package.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "wg-easy",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "server.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "serve": "DEBUG=Server PASSWORD=p WG_PATH=../config/ nodemon server.js"
 | 
			
		||||
  },
 | 
			
		||||
  "author": "Emile Nijssen",
 | 
			
		||||
  "license": "GPL",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "debug": "^4.3.1",
 | 
			
		||||
    "express": "^4.17.1",
 | 
			
		||||
    "express-session": "^1.17.1",
 | 
			
		||||
    "qrcode": "^1.4.4",
 | 
			
		||||
    "uuid": "^8.3.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "eslint": "^7.27.0",
 | 
			
		||||
    "eslint-config-athom": "^2.1.0",
 | 
			
		||||
    "nodemon": "^2.0.7"
 | 
			
		||||
  },
 | 
			
		||||
  "nodemonConfig": {
 | 
			
		||||
    "ignore": [
 | 
			
		||||
      "www/*"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": "16"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/server.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/server.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
require('./services/Server');
 | 
			
		||||
							
								
								
									
										5
									
								
								src/services/Server.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/services/Server.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
const Server = require('../lib/Server');
 | 
			
		||||
 | 
			
		||||
module.exports = new Server();
 | 
			
		||||
							
								
								
									
										5
									
								
								src/services/WireGuard.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/services/WireGuard.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
const WireGuard = require('../lib/WireGuard');
 | 
			
		||||
 | 
			
		||||
module.exports = new WireGuard();
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/www/img/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/www/img/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 67 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/www/img/favicon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/www/img/favicon.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 90 KiB  | 
							
								
								
									
										380
									
								
								src/www/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										380
									
								
								src/www/index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,380 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <title>WireGuard</title>
 | 
			
		||||
  <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
 | 
			
		||||
  <link rel="manifest" href="manifest.json">
 | 
			
		||||
  <link rel="icon" type="image/png" href="img/favicon.png">
 | 
			
		||||
  <link rel="apple-touch-icon" href="img/apple-touch-icon.png">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
 | 
			
		||||
  <meta name="apple-mobile-web-app-capable" content="yes">
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body class="bg-gray-50">
 | 
			
		||||
  <div id="app" class="container mx-auto">
 | 
			
		||||
    <div v-if="authenticated === true">
 | 
			
		||||
      <h1 class="text-4xl font-medium mt-10 mb-2">WireGuard</h1>
 | 
			
		||||
      <h2 class="text-sm text-gray-400 mb-10"><span class="cursor-pointer hover:underline" @click="logout">
 | 
			
		||||
          Logout
 | 
			
		||||
          <svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
			
		||||
            stroke="currentColor">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
              d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </span></h2>
 | 
			
		||||
 | 
			
		||||
      <div class="shadow-md rounded-lg bg-white overflow-hidden">
 | 
			
		||||
        <div class="flex flex-row flex-auto items-center p-3 px-5 border border-b-2 border-gray-100">
 | 
			
		||||
          <div class="flex-grow">
 | 
			
		||||
            <p class="text-2xl font-medium">Clients</p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="flex-shrink-0">
 | 
			
		||||
            <button @click="clientCreate = true; clientCreateName = '';"
 | 
			
		||||
              class="hover:bg-green-400 hover:border-green-400 hover:text-white text-gray-700 border-2 border-gray-100 py-2 px-4 rounded inline-flex items-center transition">
 | 
			
		||||
              <svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
			
		||||
                stroke="currentColor">
 | 
			
		||||
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
 | 
			
		||||
              </svg>
 | 
			
		||||
              <span class="text-sm">New</span>
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
          <div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id"
 | 
			
		||||
            class="p-5 flex flex-row">
 | 
			
		||||
            <div class="h-10 w-10 mr-5 rounded-full bg-gray-50 relative">
 | 
			
		||||
              <svg class="w-6 m-2 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
 | 
			
		||||
                fill="currentColor">
 | 
			
		||||
                <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
 | 
			
		||||
              </svg>
 | 
			
		||||
              <img v-if="client.avatar" :src="client.avatar" class="w-10 rounded-full absolute top-0 left-0" />
 | 
			
		||||
 | 
			
		||||
              <div v-if="client.latestHandshake && ((new Date() - new Date(client.latestHandshake) < 1000 * 60 * 10))">
 | 
			
		||||
                <div class="animate-ping w-4 h-4 p-1 bg-green-100 rounded-full absolute -bottom-1 -right-1"></div>
 | 
			
		||||
                <div class="w-2 h-2 bg-green-300 rounded-full absolute bottom-0 right-0"></div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="flex-grow">
 | 
			
		||||
              <div class="text-gray-700" :title="'Created at ' + dateTime(new Date(client.createdAt))">{{client.name}}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-if="client.allowedIps" class="text-gray-300 text-xs">{{client.iface}}
 | 
			
		||||
                · {{client.allowedIps.split('/')[0]}}
 | 
			
		||||
                <span v-if="client.transferTx" title="Download">
 | 
			
		||||
                  ·
 | 
			
		||||
                  <svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
 | 
			
		||||
                    fill="currentColor">
 | 
			
		||||
                    <path fill-rule="evenodd"
 | 
			
		||||
                      d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
 | 
			
		||||
                      clip-rule="evenodd" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  {{client.transferTx | bytes}}
 | 
			
		||||
                </span>
 | 
			
		||||
                <span v-if="client.transferRx" title="Upload">
 | 
			
		||||
                  ·
 | 
			
		||||
                  <svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
 | 
			
		||||
                    fill="currentColor">
 | 
			
		||||
                    <path fill-rule="evenodd"
 | 
			
		||||
                      d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
 | 
			
		||||
                      clip-rule="evenodd" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  {{client.transferRx | bytes}}
 | 
			
		||||
                </span>
 | 
			
		||||
                <span v-if="client.latestHandshake"
 | 
			
		||||
                  :title="'Last seen at ' + dateTime(new Date(client.latestHandshake))">
 | 
			
		||||
                  · {{new Date(client.latestHandshake) | timeago}}
 | 
			
		||||
                </span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="text-right">
 | 
			
		||||
              <div class="text-gray-400">
 | 
			
		||||
 | 
			
		||||
                <!-- Enable/Disable -->
 | 
			
		||||
                <div @click="disableClient(client)" v-if="client.enabled === true"
 | 
			
		||||
                  class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-green-400 cursor-pointer hover:bg-green-500 transition-all">
 | 
			
		||||
                  <div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div @click="enableClient(client)" v-if="client.enabled === false"
 | 
			
		||||
                  class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 cursor-pointer hover:bg-gray-300 transition-all">
 | 
			
		||||
                  <div class="rounded-full w-4 h-4 m-1 bg-white"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Show QR-->
 | 
			
		||||
                <button class="align-middle bg-gray-100 hover:bg-blue-400 hover:text-white p-2 rounded transition"
 | 
			
		||||
                  title="Show QR Code" @click="qrcode = `/api/wireguard/client/${client.id}/qrcode.svg`">
 | 
			
		||||
                  <svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
			
		||||
                    stroke="currentColor">
 | 
			
		||||
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                      d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
                <!-- Download Config -->
 | 
			
		||||
                <a :href="'/api/wireguard/client/' + client.id + '/configuration'" download
 | 
			
		||||
                  class="align-middle inline-block bg-gray-100 hover:bg-blue-400 hover:text-white p-2 rounded transition"
 | 
			
		||||
                  title="Download Configuration">
 | 
			
		||||
                  <svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
			
		||||
                    stroke="currentColor">
 | 
			
		||||
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                      d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                </a>
 | 
			
		||||
 | 
			
		||||
                <!-- Delete -->
 | 
			
		||||
                <button class="align-middle bg-gray-100 hover:bg-red-400 hover:text-white p-2 rounded transition"
 | 
			
		||||
                  title="Delete Client" @click="clientDelete = client">
 | 
			
		||||
                  <svg class="w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                    <path fill-rule="evenodd"
 | 
			
		||||
                      d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
 | 
			
		||||
                      clip-rule="evenodd" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-if="clients && clients.length === 0">
 | 
			
		||||
            <p class="text-center m-10 text-gray-400 text-sm">There are no clients yet.<br /><br />
 | 
			
		||||
              <button @click="clientCreate = true; clientCreateName = '';"
 | 
			
		||||
                class="bg-green-400 text-white hover:bg-green-500 border-2 border-none py-2 px-4 rounded inline-flex items-center transition">
 | 
			
		||||
                <svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
			
		||||
                  stroke="currentColor">
 | 
			
		||||
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                    d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
 | 
			
		||||
                </svg>
 | 
			
		||||
                <span class="text-sm">New Client</span>
 | 
			
		||||
              </button>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-if="clients === null" class="text-gray-200 p-5">
 | 
			
		||||
            <svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
 | 
			
		||||
              fill="currentColor">
 | 
			
		||||
              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
 | 
			
		||||
              <path class="opacity-75" fill="currentColor"
 | 
			
		||||
                d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
 | 
			
		||||
              </path>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- QR Code-->
 | 
			
		||||
      <div v-if="qrcode">
 | 
			
		||||
        <div class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center">
 | 
			
		||||
          <div class="bg-white rounded-md shadow-lg relative p-8">
 | 
			
		||||
            <button @click="qrcode = null" class="absolute right-4 top-4 text-gray-600 hover:text-gray-800">
 | 
			
		||||
              <svg class="w-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
			
		||||
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
 | 
			
		||||
              </svg>
 | 
			
		||||
            </button>
 | 
			
		||||
            <img :src="qrcode" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Create Dialog -->
 | 
			
		||||
      <div v-if="clientCreate" class="fixed z-10 inset-0 overflow-y-auto">
 | 
			
		||||
        <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
 | 
			
		||||
          <!--
 | 
			
		||||
        Background overlay, show/hide based on modal state.
 | 
			
		||||
 | 
			
		||||
        Entering: "ease-out duration-300"
 | 
			
		||||
          From: "opacity-0"
 | 
			
		||||
          To: "opacity-100"
 | 
			
		||||
        Leaving: "ease-in duration-200"
 | 
			
		||||
          From: "opacity-100"
 | 
			
		||||
          To: "opacity-0"
 | 
			
		||||
      -->
 | 
			
		||||
          <div class="fixed inset-0 transition-opacity" aria-hidden="true">
 | 
			
		||||
            <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- This element is to trick the browser into centering the modal contents. -->
 | 
			
		||||
          <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
 | 
			
		||||
          <!--
 | 
			
		||||
        Modal panel, show/hide based on modal state.
 | 
			
		||||
 | 
			
		||||
        Entering: "ease-out duration-300"
 | 
			
		||||
          From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
 | 
			
		||||
          To: "opacity-100 translate-y-0 sm:scale-100"
 | 
			
		||||
        Leaving: "ease-in duration-200"
 | 
			
		||||
          From: "opacity-100 translate-y-0 sm:scale-100"
 | 
			
		||||
          To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
 | 
			
		||||
      -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
 | 
			
		||||
            role="dialog" aria-modal="true" aria-labelledby="modal-headline">
 | 
			
		||||
            <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
 | 
			
		||||
              <div class="sm:flex sm:items-start">
 | 
			
		||||
                <div
 | 
			
		||||
                  class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
 | 
			
		||||
                  <svg class="h-6 w-6 text-green-600" inline xmlns="http://www.w3.org/2000/svg" fill="none"
 | 
			
		||||
                    viewBox="0 0 24 24" stroke="currentColor">
 | 
			
		||||
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                      d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
 | 
			
		||||
                  <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
 | 
			
		||||
                    New Client
 | 
			
		||||
                  </h3>
 | 
			
		||||
                  <div class="mt-2">
 | 
			
		||||
                    <p class="text-sm text-gray-500">
 | 
			
		||||
                      <input class="rounded p-2 border-2 border-gray-100 focus:border-green-200 outline-none w-full"
 | 
			
		||||
                        type="text" v-model.trim="clientCreateName" placeholder="Name" />
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
 | 
			
		||||
              <button v-if="clientCreateName.length" type="button" @click="createClient(); clientCreate = null"
 | 
			
		||||
                class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-400 text-base font-medium text-white hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm">
 | 
			
		||||
                Create
 | 
			
		||||
              </button>
 | 
			
		||||
              <button v-else type="button"
 | 
			
		||||
                class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-200 text-base font-medium text-white sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed">
 | 
			
		||||
                Create
 | 
			
		||||
              </button>
 | 
			
		||||
              <button type="button" @click="clientCreate = null"
 | 
			
		||||
                class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
 | 
			
		||||
                Cancel
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Delete Dialog -->
 | 
			
		||||
      <div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto">
 | 
			
		||||
        <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
 | 
			
		||||
          <!--
 | 
			
		||||
        Background overlay, show/hide based on modal state.
 | 
			
		||||
 | 
			
		||||
        Entering: "ease-out duration-300"
 | 
			
		||||
          From: "opacity-0"
 | 
			
		||||
          To: "opacity-100"
 | 
			
		||||
        Leaving: "ease-in duration-200"
 | 
			
		||||
          From: "opacity-100"
 | 
			
		||||
          To: "opacity-0"
 | 
			
		||||
      -->
 | 
			
		||||
          <div class="fixed inset-0 transition-opacity" aria-hidden="true">
 | 
			
		||||
            <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- This element is to trick the browser into centering the modal contents. -->
 | 
			
		||||
          <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
 | 
			
		||||
          <!--
 | 
			
		||||
        Modal panel, show/hide based on modal state.
 | 
			
		||||
 | 
			
		||||
        Entering: "ease-out duration-300"
 | 
			
		||||
          From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
 | 
			
		||||
          To: "opacity-100 translate-y-0 sm:scale-100"
 | 
			
		||||
        Leaving: "ease-in duration-200"
 | 
			
		||||
          From: "opacity-100 translate-y-0 sm:scale-100"
 | 
			
		||||
          To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
 | 
			
		||||
      -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
 | 
			
		||||
            role="dialog" aria-modal="true" aria-labelledby="modal-headline">
 | 
			
		||||
            <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
 | 
			
		||||
              <div class="sm:flex sm:items-start">
 | 
			
		||||
                <div
 | 
			
		||||
                  class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
 | 
			
		||||
                  <!-- Heroicon name: outline/exclamation -->
 | 
			
		||||
                  <svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
			
		||||
                    stroke="currentColor" aria-hidden="true">
 | 
			
		||||
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                      d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
 | 
			
		||||
                  <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
 | 
			
		||||
                    Delete Client
 | 
			
		||||
                  </h3>
 | 
			
		||||
                  <div class="mt-2">
 | 
			
		||||
                    <p class="text-sm text-gray-500">
 | 
			
		||||
                      Are you sure you want to delete <strong>{{clientDelete.name}}</strong>?
 | 
			
		||||
                      This action cannot be undone.
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
 | 
			
		||||
              <button type="button" @click="deleteClient(clientDelete); clientDelete = null"
 | 
			
		||||
                class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
 | 
			
		||||
                Delete
 | 
			
		||||
              </button>
 | 
			
		||||
              <button type="button" @click="clientDelete = null"
 | 
			
		||||
                class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
 | 
			
		||||
                Cancel
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-if="authenticated === false">
 | 
			
		||||
      <h1 class="text-4xl font-medium my-16 text-gray-700 text-center">WireGuard</h1>
 | 
			
		||||
 | 
			
		||||
      <form @submit="login" class="shadow rounded-md bg-white mx-auto w-64 p-5 overflow-hidden mt-10">
 | 
			
		||||
        <!-- Avatar -->
 | 
			
		||||
        <div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-gray-100 relative overflow-hidden">
 | 
			
		||||
          <svg class="w-10 h-10 m-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
 | 
			
		||||
            fill="currentColor">
 | 
			
		||||
            <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <input type="password" name="password" placeholder="password" v-model="password"
 | 
			
		||||
          class="px-2 py-1 text-sm text-gray-500 mb-5 border-2 border-gray-100 rounded-lg w-full focus:border-green-400 outline-none" />
 | 
			
		||||
 | 
			
		||||
        <button v-if="authenticating"
 | 
			
		||||
          class="bg-green-400 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed">
 | 
			
		||||
          <svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
 | 
			
		||||
            fill="currentColor">
 | 
			
		||||
            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
 | 
			
		||||
            <path class="opacity-75" fill="currentColor"
 | 
			
		||||
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
 | 
			
		||||
            </path>
 | 
			
		||||
          </svg>
 | 
			
		||||
        </button>
 | 
			
		||||
        <input v-if="!authenticating && password" type="submit"
 | 
			
		||||
          class="bg-green-400 w-full rounded shadow py-2 text-sm text-white hover:bg-green-500 transition cursor-pointer"
 | 
			
		||||
          value="Sign In">
 | 
			
		||||
        <input v-if="!authenticating && !password" type="submit"
 | 
			
		||||
          class="bg-gray-200 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed" value="Sign In">
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-if="authenticated === null" class="text-gray-300 pt-24 pb-12">
 | 
			
		||||
 | 
			
		||||
      <svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
 | 
			
		||||
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
 | 
			
		||||
        <path class="opacity-75" fill="currentColor"
 | 
			
		||||
          d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
 | 
			
		||||
        </path>
 | 
			
		||||
      </svg>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <p class="text-center m-10 text-gray-300 text-xs">Made by <a target="_blank" class="hover:underline"
 | 
			
		||||
        href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> · <a class="hover:underline"
 | 
			
		||||
        href="https://github.com/weejewel/wg-easy" target="_blank">GitHub</a></p>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script src="/js/vendor/vue.min.js"></script>
 | 
			
		||||
  <script src="/js/vendor/md5.min.js"></script>
 | 
			
		||||
  <script src="/js/vendor/timeago.min.js"></script>
 | 
			
		||||
  <script src="/js/api.js"></script>
 | 
			
		||||
  <script src="/js/app.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										90
									
								
								src/www/js/api.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/www/js/api.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,90 @@
 | 
			
		|||
/* eslint-disable no-unused-vars */
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class API {
 | 
			
		||||
 | 
			
		||||
  async call({ method, path, body }) {
 | 
			
		||||
    const res = await fetch(`/api${path}`, {
 | 
			
		||||
      method,
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
      },
 | 
			
		||||
      body: body
 | 
			
		||||
        ? JSON.stringify(body)
 | 
			
		||||
        : undefined,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (res.status === 204) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const json = await res.json();
 | 
			
		||||
 | 
			
		||||
    if (!res.ok) {
 | 
			
		||||
      throw new Error(json.error || res.statusText);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return json;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getSession() {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'get',
 | 
			
		||||
      path: '/session',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createSession({ password }) {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'post',
 | 
			
		||||
      path: '/session',
 | 
			
		||||
      body: { password },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deleteSession() {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'delete',
 | 
			
		||||
      path: '/session',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getClients() {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'get',
 | 
			
		||||
      path: '/wireguard/client',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createClient({ name }) {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'post',
 | 
			
		||||
      path: '/wireguard/client',
 | 
			
		||||
      body: { name },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deleteClient({ clientId }) {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'delete',
 | 
			
		||||
      path: `/wireguard/client/${clientId}`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async enableClient({ clientId }) {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'post',
 | 
			
		||||
      path: `/wireguard/client/${clientId}/enable`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async disableClient({ clientId }) {
 | 
			
		||||
    return this.call({
 | 
			
		||||
      method: 'post',
 | 
			
		||||
      path: `/wireguard/client/${clientId}/disable`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								src/www/js/app.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/www/js/app.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,140 @@
 | 
			
		|||
/* eslint-disable no-console */
 | 
			
		||||
/* eslint-disable no-alert */
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
/* eslint-disable no-new */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
new Vue({
 | 
			
		||||
  el: '#app',
 | 
			
		||||
  data: {
 | 
			
		||||
    authenticated: null,
 | 
			
		||||
    authenticating: false,
 | 
			
		||||
    password: null,
 | 
			
		||||
 | 
			
		||||
    clients: null,
 | 
			
		||||
    clientDelete: null,
 | 
			
		||||
    clientCreate: null,
 | 
			
		||||
    clientCreateName: '',
 | 
			
		||||
    qrcode: null,
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    dateTime: value => {
 | 
			
		||||
      return new Intl.DateTimeFormat(undefined, {
 | 
			
		||||
        year: 'numeric',
 | 
			
		||||
        month: 'short',
 | 
			
		||||
        day: 'numeric',
 | 
			
		||||
        hour: 'numeric',
 | 
			
		||||
        minute: 'numeric',
 | 
			
		||||
      }).format(value);
 | 
			
		||||
    },
 | 
			
		||||
    async refresh() {
 | 
			
		||||
      if (!this.authenticated) return;
 | 
			
		||||
 | 
			
		||||
      const clients = await this.api.getClients();
 | 
			
		||||
      this.clients = clients.map(client => {
 | 
			
		||||
        if (client.name.includes('@') && client.name.includes('.')) {
 | 
			
		||||
          client.avatar = `https://www.gravatar.com/avatar/${md5(client.name)}?d=blank`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return client;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    login(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
 | 
			
		||||
      if (!this.password) return;
 | 
			
		||||
      if (this.authenticating) return;
 | 
			
		||||
 | 
			
		||||
      this.authenticating = true;
 | 
			
		||||
      this.api.createSession({
 | 
			
		||||
        password: this.password,
 | 
			
		||||
      })
 | 
			
		||||
        .then(async () => {
 | 
			
		||||
          const session = await this.api.getSession();
 | 
			
		||||
          this.authenticated = session.authenticated;
 | 
			
		||||
          return this.refresh();
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
          alert(err.message || err.toString());
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          this.authenticating = false;
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    logout(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
 | 
			
		||||
      this.api.deleteSession()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.authenticated = false;
 | 
			
		||||
          this.clients = null;
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
          alert(err.message || err.toString());
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    createClient() {
 | 
			
		||||
      const name = this.clientCreateName;
 | 
			
		||||
      if (!name) return;
 | 
			
		||||
 | 
			
		||||
      this.api.createClient({ name })
 | 
			
		||||
        .catch(err => alert(err.message || err.toString()))
 | 
			
		||||
        .finally(() => this.refresh().catch(console.error));
 | 
			
		||||
    },
 | 
			
		||||
    deleteClient(clientId) {
 | 
			
		||||
      this.api.deleteClient({ clientId: client.id })
 | 
			
		||||
        .catch(err => alert(err.message || err.toString()))
 | 
			
		||||
        .finally(() => this.refresh().catch(console.error));
 | 
			
		||||
    },
 | 
			
		||||
    enableClient(client) {
 | 
			
		||||
      this.api.enableClient({ clientId: client.id })
 | 
			
		||||
        .catch(err => alert(err.message || err.toString()))
 | 
			
		||||
        .finally(() => this.refresh().catch(console.error));
 | 
			
		||||
    },
 | 
			
		||||
    disableClient(client) {
 | 
			
		||||
      this.api.disableClient({ clientId: client.id })
 | 
			
		||||
        .catch(err => alert(err.message || err.toString()))
 | 
			
		||||
        .finally(() => this.refresh().catch(console.error));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  filters: {
 | 
			
		||||
    timeago: value => {
 | 
			
		||||
      return timeago().format(value);
 | 
			
		||||
    },
 | 
			
		||||
    bytes: (bytes, decimals, kib, maxunit) => {
 | 
			
		||||
      kib = kib || false;
 | 
			
		||||
      if (bytes === 0) return '0 Bytes';
 | 
			
		||||
      if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'Not an number';
 | 
			
		||||
      const k = kib ? 1024 : 1000;
 | 
			
		||||
      const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2;
 | 
			
		||||
      const sizes = kib
 | 
			
		||||
        ? ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB']
 | 
			
		||||
        : ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
 | 
			
		||||
      let i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
      if (maxunit !== undefined) {
 | 
			
		||||
        const index = sizes.indexOf(maxunit);
 | 
			
		||||
        if (index !== -1) i = index;
 | 
			
		||||
      }
 | 
			
		||||
      // eslint-disable-next-line no-restricted-properties
 | 
			
		||||
      return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.api = new API();
 | 
			
		||||
    this.api.getSession()
 | 
			
		||||
      .then(session => {
 | 
			
		||||
        this.authenticated = session.authenticated;
 | 
			
		||||
        this.refresh().catch(err => {
 | 
			
		||||
          alert(err.message || err.toString());
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        alert(err.message || err.toString());
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    setInterval(() => {
 | 
			
		||||
      this.refresh().catch(console.error);
 | 
			
		||||
    }, 1000);
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										1
									
								
								src/www/js/vendor/md5.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/www/js/vendor/md5.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
!function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<<t%32,n[14+(t+64>>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h<n.length;h+=16)c=l(r=c,e=f,o=i,u=a,n[h],7,-680876936),a=l(a,c,f,i,n[h+1],12,-389564586),i=l(i,a,c,f,n[h+2],17,606105819),f=l(f,i,a,c,n[h+3],22,-1044525330),c=l(c,f,i,a,n[h+4],7,-176418897),a=l(a,c,f,i,n[h+5],12,1200080426),i=l(i,a,c,f,n[h+6],17,-1473231341),f=l(f,i,a,c,n[h+7],22,-45705983),c=l(c,f,i,a,n[h+8],7,1770035416),a=l(a,c,f,i,n[h+9],12,-1958414417),i=l(i,a,c,f,n[h+10],17,-42063),f=l(f,i,a,c,n[h+11],22,-1990404162),c=l(c,f,i,a,n[h+12],7,1804603682),a=l(a,c,f,i,n[h+13],12,-40341101),i=l(i,a,c,f,n[h+14],17,-1502002290),c=v(c,f=l(f,i,a,c,n[h+15],22,1236535329),i,a,n[h+1],5,-165796510),a=v(a,c,f,i,n[h+6],9,-1069501632),i=v(i,a,c,f,n[h+11],14,643717713),f=v(f,i,a,c,n[h],20,-373897302),c=v(c,f,i,a,n[h+5],5,-701558691),a=v(a,c,f,i,n[h+10],9,38016083),i=v(i,a,c,f,n[h+15],14,-660478335),f=v(f,i,a,c,n[h+4],20,-405537848),c=v(c,f,i,a,n[h+9],5,568446438),a=v(a,c,f,i,n[h+14],9,-1019803690),i=v(i,a,c,f,n[h+3],14,-187363961),f=v(f,i,a,c,n[h+8],20,1163531501),c=v(c,f,i,a,n[h+13],5,-1444681467),a=v(a,c,f,i,n[h+2],9,-51403784),i=v(i,a,c,f,n[h+7],14,1735328473),c=g(c,f=v(f,i,a,c,n[h+12],20,-1926607734),i,a,n[h+5],4,-378558),a=g(a,c,f,i,n[h+8],11,-2022574463),i=g(i,a,c,f,n[h+11],16,1839030562),f=g(f,i,a,c,n[h+14],23,-35309556),c=g(c,f,i,a,n[h+1],4,-1530992060),a=g(a,c,f,i,n[h+4],11,1272893353),i=g(i,a,c,f,n[h+7],16,-155497632),f=g(f,i,a,c,n[h+10],23,-1094730640),c=g(c,f,i,a,n[h+13],4,681279174),a=g(a,c,f,i,n[h],11,-358537222),i=g(i,a,c,f,n[h+3],16,-722521979),f=g(f,i,a,c,n[h+6],23,76029189),c=g(c,f,i,a,n[h+9],4,-640364487),a=g(a,c,f,i,n[h+12],11,-421815835),i=g(i,a,c,f,n[h+15],16,530742520),c=m(c,f=g(f,i,a,c,n[h+2],23,-995338651),i,a,n[h],6,-198630844),a=m(a,c,f,i,n[h+7],10,1126891415),i=m(i,a,c,f,n[h+14],15,-1416354905),f=m(f,i,a,c,n[h+5],21,-57434055),c=m(c,f,i,a,n[h+12],6,1700485571),a=m(a,c,f,i,n[h+3],10,-1894986606),i=m(i,a,c,f,n[h+10],15,-1051523),f=m(f,i,a,c,n[h+1],21,-2054922799),c=m(c,f,i,a,n[h+8],6,1873313359),a=m(a,c,f,i,n[h+15],10,-30611744),i=m(i,a,c,f,n[h+6],15,-1560198380),f=m(f,i,a,c,n[h+13],21,1309151649),c=m(c,f,i,a,n[h+4],6,-145523070),a=m(a,c,f,i,n[h+11],10,-1120210379),i=m(i,a,c,f,n[h+2],15,718787259),f=m(f,i,a,c,n[h+9],21,-343485551),c=d(c,r),f=d(f,e),i=d(i,o),a=d(a,u);return[c,f,i,a]}function a(n){for(var t="",r=32*n.length,e=0;e<r;e+=8)t+=String.fromCharCode(n[e>>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e<t.length;e+=1)t[e]=0;for(var r=8*n.length,e=0;e<r;e+=8)t[e>>5]|=(255&n.charCodeAt(e/8))<<e%32;return t}function e(n){for(var t,r="0123456789abcdef",e="",o=0;o<n.length;o+=1)t=n.charCodeAt(o),e+=r.charAt(t>>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16<o.length&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}(r(n),r(t))}function t(n,t,r){return t?r?u(t,n):e(u(t,n)):r?o(n):e(o(n))}"function"==typeof define&&define.amd?define(function(){return t}):"object"==typeof module&&module.exports?module.exports=t:n.md5=t}(this);
 | 
			
		||||
							
								
								
									
										1
									
								
								src/www/js/vendor/timeago.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/www/js/vendor/timeago.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
!function(t,e){"object"==typeof module&&module.exports?module.exports=e(t):t.timeago=e(t)}("undefined"!=typeof window?window:this,function(){function t(t){return t instanceof Date?t:isNaN(t)?/^\d+$/.test(t)?new Date(e(t,10)):(t=(t||"").trim().replace(/\.\d+/,"").replace(/-/,"/").replace(/-/,"/").replace(/T/," ").replace(/Z/," UTC").replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"),new Date(t)):new Date(e(t))}function e(t){return parseInt(t)}function n(t,n,r){n=d[n]?n:d[r]?r:"en";var i=0;for(agoin=t<0?1:0,t=Math.abs(t);t>=l[i]&&i<p;i++)t/=l[i];return t=e(t),i*=2,t>(0===i?9:1)&&(i+=1),d[n](t,i)[agoin].replace("%s",t)}function r(e,n){return n=n?t(n):new Date,(n-t(e))/1e3}function i(t){for(var e=1,n=0,r=Math.abs(t);t>=l[n]&&n<p;n++)t/=l[n],e*=l[n];return r%=e,r=r?e-r:e,Math.ceil(r)}function o(t){return t.getAttribute?t.getAttribute(h):t.attr?t.attr(h):void 0}function a(t,e){function a(o,c,f,s){var d=r(c,t);o.innerHTML=n(d,f,e),u["k"+s]=setTimeout(function(){a(o,c,f,s)},1e3*i(d))}var u={};return e||(e="en"),this.format=function(i,o){return n(r(i,t),o,e)},this.render=function(t,e){void 0===t.length&&(t=[t]);for(var n=0;n<t.length;n++)a(t[n],o(t[n]),e,++c)},this.cancel=function(){for(var t in u)clearTimeout(u[t]);u={}},this.setLocale=function(t){e=t},this}function u(t,e){return new a(t,e)}var c=0,f="second_minute_hour_day_week_month_year".split("_"),s="秒_分钟_小时_天_周_月_年".split("_"),d={en:function(t,e){if(0===e)return["just now","right now"];var n=f[parseInt(e/2)];return t>1&&(n+="s"),[t+" "+n+" ago","in "+t+" "+n]},zh_CN:function(t,e){if(0===e)return["刚刚","片刻后"];var n=s[parseInt(e/2)];return[t+n+"前",t+n+"后"]}},l=[60,60,24,7,365/7/12,12],p=6,h="datetime";return u.register=function(t,e){d[t]=e},u});
 | 
			
		||||
							
								
								
									
										6
									
								
								src/www/js/vendor/vue.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/www/js/vendor/vue.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										11
									
								
								src/www/manifest.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/www/manifest.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "WireGuard",
 | 
			
		||||
  "display": "standalone",
 | 
			
		||||
  "background_color": "#fff",
 | 
			
		||||
  "icons": [
 | 
			
		||||
    {
 | 
			
		||||
      "src": "img/favicon.png",
 | 
			
		||||
      "type": "image/png"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue