<!DOCTYPE html>
<html lang="en">
<head>
<title>wg-api-web</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/gh/tofsjonas/sortable/sortable-base.min.css" rel="stylesheet">
<style>
html {
font-size: 16px;
font-family: sans-serif;
}
body {
margin: 2rem;
}
@media(max-width: 600px) {
body {
margin: .5rem;
}
}
table {
display: block;
overflow-x: auto;
white-space: nowrap;
margin-bottom: 1.5rem;
border-spacing: 0;
}
th, td {
padding: 3px 8px;
}
th {
text-align: center;
border-bottom: 2px solid black;
}
td, th[scope="row"] {
text-align: left;
border-bottom: 1px solid black;
}
.sortable th.no-sort {
pointer-events: none;
}
</style>
</head>
<body>
<main>
<table>
<tbody>
<tr id="device_name">
<th scope="row">Device</th>
<td>-</td>
</tr>
<tr id="device_public_key">
<th scope="row">Public key</th>
<td>-</td>
</tr>
<tr id="device_listen_port">
<th scope="row">Listen port</th>
<td>-</td>
</tr>
<tr id="device_num_peers">
<th scope="row"># of peers</th>
<td>-</td>
</tr>
</tbody>
</table>
<table id="peers" class="sortable">
<thead>
<tr>
<th>Peer</th>
<th>Last handshake</th>
<th class="no-sort">Endpoint</th>
<th>Rx</th>
<th>Tx</th>
<th class="no-sort">Allowed IPs</th>
<th>Preshared key?</th>
</tr>
</thead>
<tbody id="tbody_peers">
</tbody>
</table>
</main>
<script>
const KB = 1024;
const MB = 1024 * KB;
const GB = 1024 * MB;
function bytes_to_readable(bytes) {
if (bytes == 0)
return '-';
if (bytes < MB)
return '< 1 MiB';
if (bytes < GB)
return `${Math.round(bytes / MB)} MiB`;
let result = (bytes / GB).toFixed(1);
if (result.endsWith('.0'))
result = result.substring(0, result.length - 2);
return `${result} GiB`;
}
const MSECS_IN_SEC = 1000;
const MSECS_IN_MIN = 60 * MSECS_IN_SEC;
const MSECS_IN_HOUR = 60 * MSECS_IN_MIN;
const MSECS_IN_DAY = 24 * MSECS_IN_HOUR;
function date_to_readable(date) {
const now = Date.now();
let duration = now - date;
if (Math.abs(duration) < 2 * MSECS_IN_SEC) {
// Most likely, the time is not synchronized either on the server or
// the client.
return 'now';
}
if (duration < 0) {
return 'future???';
}
let days = Math.floor(duration / MSECS_IN_DAY);
duration -= days * MSECS_IN_DAY;
let hours = Math.floor(duration / MSECS_IN_HOUR);
duration -= hours * MSECS_IN_HOUR;
let mins = Math.floor(duration / MSECS_IN_MIN);
duration -= mins * MSECS_IN_MIN;
let secs = Math.floor(duration / MSECS_IN_SEC);
duration -= secs * MSECS_IN_SEC;
if (days > 364)
return '-';
if (days > 0) {
let result = `${days}d`;
if (days == 1 && hours > 0)
result += ` ${hours}h`;
return result;
}
if (hours > 0) {
let result = `${hours}h`;
if (hours == 1 && mins > 0)
result += ` ${mins}m`;
return result;
}
if (mins > 0) {
let result = `${mins}m`;
if (mins == 1 && secs > 0)
result += ` ${secs}s`;
return result;
}
return `${secs}s`;
}
function in_code(text) {
let code = document.createElement('code');
code.appendChild(document.createTextNode(text));
return code;
}
function send_request(endpoint, callback) {
let request = new XMLHttpRequest();
request.addEventListener('load', callback);
request.open('GET', 'api/' + endpoint);
request.send();
}
var Field = function(key) {
this.key = key;
}
Field.prototype.cell_container = function() {
return document.createElement('td');
}
Field.prototype.sortable_value = function(value) {
return value;
}
Field.prototype.cell_contents = function(value) {
return [document.createTextNode(this.sortable_value(value))];
}
Field.prototype.create_cell = function(peer) {
let cell = this.cell_container();
let value = peer[this.key];
let sortable = this.sortable_value(value);
cell.setAttribute("data-sort", sortable);
cell.setAttribute("title", sortable);
let contents = this.cell_contents(value);
contents.forEach(function(elem) {
cell.appendChild(elem);
});
return cell;
}
var PublicKey = function() {
Field.call(this, 'public_key');
}
PublicKey.prototype = Object.create(Field.prototype);
PublicKey.prototype.constructor = PublicKey;
PublicKey.prototype.sortable_value = function(value) {
if (value in aliases)
return aliases[value];
return value;
}
PublicKey.prototype.cell_contents = function(value) {
return [in_code(this.sortable_value(value))];
}
var LastHandshake = function() {
Field.call(this, 'last_handshake');
}
LastHandshake.prototype = Object.create(Field.prototype);
LastHandshake.prototype.constructor = LastHandshake;
LastHandshake.prototype.cell_contents = function(value) {
value = this.sortable_value(value);
value = Date.parse(value);
value = date_to_readable(value);
return Field.prototype.cell_contents.call(this, value);
}
var Endpoint = function() {
Field.call(this, 'endpoint');
}
Endpoint.prototype = Object.create(Field.prototype);
Endpoint.prototype.constructor = Endpoint;
Endpoint.prototype.sortable_value = function(value) {
if (value == "<nil>")
return '-';
return value;
}
Endpoint.prototype.cell_contents = function(value) {
value = this.sortable_value(value);
if (value == '' || value == '-')
return Field.prototype.cell_contents.call(this, value);
let ip = value;
let idx = value.lastIndexOf(':');
if (idx >= 0)
ip = value.substring(0, idx);
if (ip.charAt(0) == '[' && ip.slice(-1) == ']')
ip = ip.slice(1, -1);
let elem = document.createElement('a');
elem.setAttribute('href', 'https://infobyip.com/ip-' + ip + '.html');
elem.setAttribute('target', '_blank');
elem.appendChild(document.createTextNode(value));
return [elem];
}
var Rx = function() {
Field.call(this, 'receive_bytes');
}
Rx.prototype = Object.create(Field.prototype);
Rx.prototype.constructor = Rx;
Rx.prototype.cell_contents = function(value) {
value = this.sortable_value(value);
value = parseInt(value);
value = bytes_to_readable(value);
return Field.prototype.cell_contents.call(this, value);
}
var Tx = function() {
Field.call(this, 'transmit_bytes');
}
Tx.prototype = Object.create(Field.prototype);
Tx.prototype.constructor = Tx;
Tx.prototype.cell_contents = Rx.prototype.cell_contents;
var AllowedIPs = function() {
Field.call(this, 'allowed_ips');
}
AllowedIPs.prototype = Object.create(Field.prototype);
AllowedIPs.prototype.constructor = AllowedIPs;
AllowedIPs.prototype.sortable_value = function(value) {
return value.join(", ");
}
AllowedIPs.prototype.cell_contents = function(value) {
let result = [];
value.forEach(function(ip) {
result.push(in_code(ip));
result.push(document.createElement('br'));
});
return result;
}
var HasPresharedKey = function() {
Field.call(this, 'has_preshared_key');
}
HasPresharedKey.prototype = Object.create(Field.prototype);
HasPresharedKey.prototype.constructor = HasPresharedKey;
HasPresharedKey.prototype.cell_contents = function(value) {
if (value) {
value = '\u2714';
} else {
value = '\u2718';
}
return Field.prototype.cell_contents.call(this, value);
}
var Device = function() {
this.fields = [
new Field('name'),
new PublicKey(),
new Field('listen_port'),
new Field('num_peers'),
];
}
Device.prototype.update = function(data) {
this.fields.forEach(function(field) {
var row = document.getElementById('device_' + field.key);
row.removeChild(row.lastElementChild);
row.appendChild(field.create_cell(data));
});
}
function peers_get_tbody() {
return document.getElementById('tbody_peers');
}
var Peer = function() {
this.fields = [
new PublicKey(),
new LastHandshake(),
new Endpoint(),
new Rx(),
new Tx(),
new AllowedIPs(),
new HasPresharedKey()
];
this.table = peers_get_tbody();
}
Peer.prototype.create_row = function(peer) {
let row = document.createElement('tr');
this.fields.forEach(function(field) {
row.appendChild(field.create_cell(peer));
});
return row;
}
Peer.prototype.add_row = function(peer) {
this.table.appendChild(this.create_row(peer));
}
function peers_remove_all() {
let table = peers_get_tbody();
while (table.firstChild) {
table.removeChild(table.firstChild);
}
}
function device_show() {
let formatter = new Device();
let data = JSON.parse(this.responseText);
data = data['result']['device'];
document.title = `${data['name']} - ${data['num_peers']} peer(s) - wg-api-web`;
formatter.update(data);
}
function peers_show() {
peers_remove_all();
let formatter = new Peer();
let data = JSON.parse(this.responseText);
data['result']['peers'].forEach(function(peer) {
formatter.add_row(peer);
});
peers_table_sort();
}
var aliases = {};
function aliases_parse(data) {
aliases = {};
data.split(/\r?\n/).forEach(function(line) {
let delim = line.match(/\s+/);
if (!delim)
return;
let key = line.slice(0, delim.index);
let alias = line.slice(delim.index + delim[0].length);
aliases[key] = alias;
});
}
function device_update() {
send_request('GetDeviceInfo', device_show);
}
function peers_update() {
send_request('ListPeers', peers_show);
}
function aliases_update() {
send_request('aliases', function() {
aliases_parse(this.responseText);
peers_update();
});
}
function update() {
device_update();
aliases_update();
}
var update_interval_seconds = 30;
function loop() {
setInterval(function() {
update();
}, update_interval_seconds * 1000);
}
// The number of the column last used for sorting (1-based indexing).
// A positive number means ascending order; negative number means descending
// order.
var peers_last_sorted_by = 0;
function peers_table_sort() {
if (!peers_last_sorted_by)
return;
let th = document.querySelector(`#peers thead tr :nth-child(${Math.abs(peers_last_sorted_by)})`);
th.click();
th.click();
}
function peers_table_save_sorting(th) {
let idx = th.cellIndex + 1;
if (Math.abs(peers_last_sorted_by) == idx) {
peers_last_sorted_by = -peers_last_sorted_by;
} else {
peers_last_sorted_by = idx;
}
}
function peers_table_setup() {
document.querySelectorAll('#peers th').forEach(function(th) {
th.addEventListener('click', function(event) {
peers_table_save_sorting(event.target);
}, false);
});
}
function main() {
peers_table_setup();
update();
loop();
}
main();
</script>
<script src="https://cdn.jsdelivr.net/gh/tofsjonas/sortable/sortable.min.js"></script>
</body>
</html>