138 lines
4.7 KiB
JavaScript
138 lines
4.7 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Lethean VPN Config API
|
||
|
|
*
|
||
|
|
* Simple HTTP API that serves WireGuard peer configs for beta testers.
|
||
|
|
* In production this sits behind Blesta auth. For testnet it's open.
|
||
|
|
*
|
||
|
|
* Endpoints:
|
||
|
|
* GET / — info page
|
||
|
|
* GET /api/peer/:name — get WireGuard config for a peer
|
||
|
|
* GET /api/peers — list available peers
|
||
|
|
* GET /api/status — gateway status
|
||
|
|
*/
|
||
|
|
|
||
|
|
const http = require('http');
|
||
|
|
const { execFileSync } = require('child_process');
|
||
|
|
|
||
|
|
const PORT = process.env.PORT || 8100;
|
||
|
|
const WG_CONTAINER = process.env.WG_CONTAINER || 'lthn-wireguard';
|
||
|
|
const DAEMON_URL = process.env.DAEMON_URL || 'http://127.0.0.1:46941';
|
||
|
|
|
||
|
|
function jsonResponse(res, data, status = 200) {
|
||
|
|
res.writeHead(status, {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Access-Control-Allow-Origin': '*',
|
||
|
|
});
|
||
|
|
res.end(JSON.stringify(data, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
function dockerExec(args) {
|
||
|
|
try {
|
||
|
|
return execFileSync('docker', ['exec', WG_CONTAINER, ...args], {
|
||
|
|
encoding: 'utf8',
|
||
|
|
timeout: 10000,
|
||
|
|
}).trim();
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function listPeerConfigs() {
|
||
|
|
const list = dockerExec(['ls', '/config/']);
|
||
|
|
if (!list) return [];
|
||
|
|
return list.split('\n')
|
||
|
|
.filter(d => d.startsWith('peer_'))
|
||
|
|
.map(d => d.replace('peer_', ''));
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPeerConfig(peerName) {
|
||
|
|
const safeName = peerName.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 20);
|
||
|
|
if (!safeName) return null;
|
||
|
|
return dockerExec(['cat', `/config/peer_${safeName}/peer_${safeName}.conf`]);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPeers() {
|
||
|
|
const dump = dockerExec(['wg', 'show', 'wg0', 'dump']);
|
||
|
|
if (!dump) return [];
|
||
|
|
return dump.split('\n').slice(1).map(line => {
|
||
|
|
const fields = line.split('\t');
|
||
|
|
if (fields.length < 7) return null;
|
||
|
|
return {
|
||
|
|
pubkey: fields[0]?.slice(0, 16) + '...',
|
||
|
|
endpoint: fields[2] || 'none',
|
||
|
|
active: parseInt(fields[4]) > 0 && (Date.now() / 1000 - parseInt(fields[4])) < 300,
|
||
|
|
rxBytes: parseInt(fields[5]) || 0,
|
||
|
|
txBytes: parseInt(fields[6]) || 0,
|
||
|
|
};
|
||
|
|
}).filter(Boolean);
|
||
|
|
}
|
||
|
|
|
||
|
|
const server = http.createServer(async (req, res) => {
|
||
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||
|
|
|
||
|
|
if (url.pathname === '/' && req.method === 'GET') {
|
||
|
|
const peers = listPeerConfigs();
|
||
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||
|
|
return res.end(`<!DOCTYPE html><html><head><title>Lethean VPN</title>
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
|
<style>body{font-family:monospace;background:#0a0a0a;color:#e0e0e0;max-width:600px;margin:40px auto;padding:20px}
|
||
|
|
h1{color:#00d4aa}a{color:#00d4aa}pre{background:#111;padding:15px;border-radius:6px;overflow-x:auto}
|
||
|
|
.peer{background:#111;padding:15px;margin:10px 0;border-radius:8px;border-left:3px solid #00d4aa}
|
||
|
|
.peer a{font-size:18px}</style></head>
|
||
|
|
<body><h1>Lethean Testnet VPN</h1>
|
||
|
|
<p>Download a WireGuard config and connect to the Lethean network.</p>
|
||
|
|
<h2>Available Configs</h2>
|
||
|
|
${peers.map(p => `<div class="peer"><a href="/api/peer/${p}">Download: ${p}.conf</a></div>`).join('\n')}
|
||
|
|
<h2>How to connect</h2>
|
||
|
|
<pre># Download config
|
||
|
|
curl -o lethean-vpn.conf http://localhost:${PORT}/api/peer/${peers[0] || 'testpeer1'}
|
||
|
|
|
||
|
|
# Connect (Linux)
|
||
|
|
sudo wg-quick up ./lethean-vpn.conf
|
||
|
|
|
||
|
|
# Or import into WireGuard app (Windows/Mac/iOS/Android)</pre>
|
||
|
|
<p><a href="/api/status">Gateway Status</a> · <a href="/api/peers">Active Peers</a></p>
|
||
|
|
</body></html>`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// GET /api/peer/:name — download config
|
||
|
|
const peerMatch = url.pathname.match(/^\/api\/peer\/([a-zA-Z0-9_-]+)$/);
|
||
|
|
if (peerMatch && req.method === 'GET') {
|
||
|
|
const config = getPeerConfig(peerMatch[1]);
|
||
|
|
if (!config) return jsonResponse(res, { error: 'Peer not found' }, 404);
|
||
|
|
res.writeHead(200, {
|
||
|
|
'Content-Type': 'text/plain',
|
||
|
|
'Content-Disposition': `attachment; filename="${peerMatch[1]}.conf"`,
|
||
|
|
});
|
||
|
|
return res.end(config);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (url.pathname === '/api/peers' && req.method === 'GET') {
|
||
|
|
return jsonResponse(res, { available: listPeerConfigs(), connected: getPeers() });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (url.pathname === '/api/status' && req.method === 'GET') {
|
||
|
|
const peers = getPeers();
|
||
|
|
let chainHeight = null;
|
||
|
|
try {
|
||
|
|
const resp = await fetch(`${DAEMON_URL}/json_rpc`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ jsonrpc: '2.0', id: '0', method: 'getinfo' })
|
||
|
|
});
|
||
|
|
chainHeight = (await resp.json()).result?.height;
|
||
|
|
} catch {}
|
||
|
|
return jsonResponse(res, {
|
||
|
|
chain: { height: chainHeight },
|
||
|
|
vpn: { total: peers.length, active: peers.filter(p => p.active).length, configs: listPeerConfigs().length },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
jsonResponse(res, { error: 'Not found' }, 404);
|
||
|
|
});
|
||
|
|
|
||
|
|
server.listen(PORT, () => {
|
||
|
|
console.log(`Lethean VPN Config API on :${PORT}`);
|
||
|
|
});
|