229 lines
9.9 KiB
PHP
229 lines
9.9 KiB
PHP
<?php
|
||
require_once __DIR__.'/inc/common.php';
|
||
|
||
$SSH_KEY = '/var/www/.ssh/nordvpn';
|
||
|
||
$WIREGUARD = ['host' => '192.168.10.100', 'user' => 'root', 'key' => $SSH_KEY];
|
||
|
||
$NORDVPNS = [
|
||
'lxc104' => ['label' => 'YouTube', 'icon' => '📺', 'host' => '192.168.10.104', 'desc' => 'WireGuard'],
|
||
'lxc110' => ['label' => 'Torrents', 'icon' => '🧲', 'host' => '192.168.10.110', 'desc' => 'Transmission'],
|
||
];
|
||
|
||
$COUNTRIES = [
|
||
'Albania' => '🇦🇱', 'Australia' => '🇦🇺', 'Austria' => '🇦🇹',
|
||
'Belgium' => '🇧🇪', 'Brazil' => '🇧🇷', 'Bulgaria' => '🇧🇬',
|
||
'Canada' => '🇨🇦', 'Chile' => '🇨🇱', 'Colombia' => '🇨🇴',
|
||
'Croatia' => '🇭🇷', 'Czech Republic' => '🇨🇿', 'Denmark' => '🇩🇰',
|
||
'Estonia' => '🇪🇪', 'Finland' => '🇫🇮', 'France' => '🇫🇷',
|
||
'Germany' => '🇩🇪', 'Greece' => '🇬🇷', 'Hong Kong' => '🇭🇰',
|
||
'Hungary' => '🇭🇺', 'Iceland' => '🇮🇸', 'India' => '🇮🇳',
|
||
'Ireland' => '🇮🇪', 'Israel' => '🇮🇱', 'Italy' => '🇮🇹',
|
||
'Japan' => '🇯🇵', 'Latvia' => '🇱🇻', 'Lithuania' => '🇱🇹',
|
||
'Luxembourg' => '🇱🇺', 'Malaysia' => '🇲🇾', 'Mexico' => '🇲🇽',
|
||
'Moldova' => '🇲🇩', 'Netherlands' => '🇳🇱', 'New Zealand' => '🇳🇿',
|
||
'Nigeria' => '🇳🇬', 'Norway' => '🇳🇴', 'Philippines' => '🇵🇭',
|
||
'Poland' => '🇵🇱', 'Portugal' => '🇵🇹', 'Romania' => '🇷🇴',
|
||
'Serbia' => '🇷🇸', 'Singapore' => '🇸🇬', 'Slovakia' => '🇸🇰',
|
||
'Slovenia' => '🇸🇮', 'South Africa' => '🇿🇦', 'South Korea' => '🇰🇷',
|
||
'Spain' => '🇪🇸', 'Sweden' => '🇸🇪', 'Switzerland' => '🇨🇭',
|
||
'Taiwan' => '🇹🇼', 'Thailand' => '🇹🇭', 'Turkey' => '🇹🇷',
|
||
'Ukraine' => '🇺🇦', 'United Kingdom' => '🇬🇧', 'United States' => '🇺🇸',
|
||
];
|
||
|
||
function do_ssh(string $host, string $user, string $key, string $cmd): string {
|
||
$opts = "-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8";
|
||
$ssh = "/usr/bin/ssh -i ".escapeshellarg($key)." $opts "
|
||
.escapeshellarg("$user@$host")." ".escapeshellarg($cmd)." 2>&1";
|
||
return trim((string)shell_exec($ssh));
|
||
}
|
||
|
||
function parse_nordvpn_status(string $raw): array {
|
||
$connected = stripos($raw, 'Connected') !== false;
|
||
$country = '—'; $ip = '—';
|
||
if (preg_match('/Country:\s*(.+)/i', $raw, $m)) $country = trim($m[1]);
|
||
if (preg_match('/IP:\s*(\S+)/i', $raw, $m)) $ip = trim($m[1]);
|
||
return compact('connected', 'country', 'ip');
|
||
}
|
||
|
||
function parse_wg_status(string $raw): array {
|
||
$parts = preg_split('/=== (wg\d+) ===/i', $raw, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||
$ifaces = [];
|
||
for ($i = 1; $i < count($parts) - 1; $i += 2) {
|
||
$name = $parts[$i];
|
||
$block = $parts[$i + 1];
|
||
$peers = substr_count($block, 'peer:');
|
||
$active = preg_match_all('/latest handshake: (\d+) (second|minute|hour)/i', $block);
|
||
$ip = '—';
|
||
$ext = '—';
|
||
if (preg_match('/addr:\s*(\S+)/i', $block, $m)) $ip = $m[1];
|
||
if (preg_match('/ext:\s*(\S+)/i', $block, $m)) $ext = $m[1];
|
||
$ifaces[$name] = ['peers' => $peers, 'active' => $active, 'ip' => $ip, 'ext' => $ext];
|
||
}
|
||
return $ifaces;
|
||
}
|
||
|
||
// AJAX
|
||
if (isset($_GET['ajax'])) {
|
||
header('Content-Type: application/json');
|
||
$id = $_GET['ajax'];
|
||
if ($id === 'wg') {
|
||
$raw = do_ssh($WIREGUARD['host'], $WIREGUARD['user'], $WIREGUARD['key'], 'status');
|
||
echo json_encode(['ifaces' => parse_wg_status($raw)]);
|
||
} elseif (isset($NORDVPNS[$id])) {
|
||
$raw = do_ssh($NORDVPNS[$id]['host'], 'root', $SSH_KEY, 'status');
|
||
echo json_encode(parse_nordvpn_status($raw));
|
||
}
|
||
exit;
|
||
}
|
||
|
||
$result = null;
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$section = $_POST['section'] ?? '';
|
||
$action = $_POST['action'] ?? '';
|
||
if (isset($NORDVPNS[$section])) {
|
||
$vpn = $NORDVPNS[$section];
|
||
if ($action === 'connect') {
|
||
$country = $_POST['country'] ?? '';
|
||
if (isset($COUNTRIES[$country])) {
|
||
$raw = do_ssh($vpn['host'], 'root', $SSH_KEY, "connect $country");
|
||
$result = ['id' => $section, 'ok' => stripos($raw, 'connected') !== false, 'msg' => $raw];
|
||
}
|
||
} elseif ($action === 'disconnect') {
|
||
$raw = do_ssh($vpn['host'], 'root', $SSH_KEY, 'disconnect');
|
||
$result = ['id' => $section, 'ok' => true, 'msg' => $raw];
|
||
}
|
||
}
|
||
}
|
||
|
||
$title = 'VPN';
|
||
ob_start();
|
||
?>
|
||
|
||
<div class="page-header">
|
||
<div>
|
||
<div class="breadcrumb"><a href="/painel.php">Painel</a><span class="sep">›</span>VPN</div>
|
||
<h1>VPN</h1>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($result): ?>
|
||
<div class="alert <?= $result['ok'] ? 'alert-success' : 'alert-error' ?>" style="margin-bottom:16px">
|
||
<?= $result['ok'] ? '✓' : '⚠' ?> <?= h($result['msg']) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px">
|
||
|
||
<!-- NordVPN cards -->
|
||
<?php foreach ($NORDVPNS as $id => $vpn): ?>
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2><?= $vpn['icon'] ?> <?= h($vpn['label']) ?></h2>
|
||
<span style="font-size:11px;color:var(--muted)"><?= h($vpn['desc']) ?></span>
|
||
</div>
|
||
<div class="card-body" style="display:flex;flex-direction:column;gap:14px">
|
||
|
||
<!-- Estado resumido -->
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<div>
|
||
<div id="flag-<?= $id ?>" style="font-size:22px;line-height:1">—</div>
|
||
<div id="country-<?= $id ?>" style="font-size:13px;font-weight:500;margin-top:4px">A carregar…</div>
|
||
<div id="ip-<?= $id ?>" style="font-size:11px;color:var(--muted);font-family:monospace"></div>
|
||
</div>
|
||
<div id="dot-<?= $id ?>" style="width:10px;height:10px;border-radius:50%;background:var(--muted);flex-shrink:0"></div>
|
||
</div>
|
||
|
||
<!-- Dropdown + ações -->
|
||
<form method="post" style="display:flex;gap:8px">
|
||
<input type="hidden" name="section" value="<?= $id ?>">
|
||
<input type="hidden" name="action" value="connect">
|
||
<select name="country" required style="flex:1;padding:7px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none">
|
||
<option value="" disabled selected>Mudar país…</option>
|
||
<?php foreach ($COUNTRIES as $name => $flag): ?>
|
||
<option value="<?= h($name) ?>"><?= $flag ?> <?= h($name) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="btn btn-primary btn-sm">Ligar</button>
|
||
</form>
|
||
|
||
<form method="post">
|
||
<input type="hidden" name="section" value="<?= $id ?>">
|
||
<input type="hidden" name="action" value="disconnect">
|
||
<button type="submit" class="btn btn-danger btn-sm" style="width:100%">Desligar</button>
|
||
</form>
|
||
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- WireGuard -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2>🔒 WireGuard</h2>
|
||
<span style="font-size:11px;color:var(--muted)">LXC 100</span>
|
||
</div>
|
||
<div class="card-body" style="display:flex;flex-direction:column;gap:12px">
|
||
<?php foreach (['wg0' => 'Direto', 'wg1' => 'NordVPN'] as $iface => $desc): ?>
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<div id="dot-<?= $iface ?>" style="width:9px;height:9px;border-radius:50%;background:var(--muted);flex-shrink:0"></div>
|
||
<div>
|
||
<div style="font-size:13px;font-weight:600"><?= $iface ?> <span style="font-weight:400;color:var(--muted);font-size:12px">— <?= $desc ?></span></div>
|
||
<div id="<?= $iface ?>-info" style="font-size:11px;color:var(--muted)">A carregar…</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
const FLAGS = <?= json_encode(array_map(fn($f) => $f, $COUNTRIES), JSON_UNESCAPED_UNICODE) ?>;
|
||
const NAMES = <?= json_encode(array_keys($COUNTRIES)) ?>;
|
||
|
||
function getFlag(country) {
|
||
const i = NAMES.indexOf(country);
|
||
return i >= 0 ? Object.values(FLAGS)[i] : '🌐';
|
||
}
|
||
|
||
function loadNord(id) {
|
||
fetch('/vpn.php?ajax=' + id).then(r => r.json()).then(d => {
|
||
document.getElementById('flag-' + id).textContent = d.connected ? getFlag(d.country) : '—';
|
||
document.getElementById('country-' + id).textContent = d.connected ? d.country : 'Desligado';
|
||
document.getElementById('ip-' + id).textContent = d.connected ? d.ip : '';
|
||
document.getElementById('dot-' + id).style.background = d.connected ? '#3fb950' : '#6e2120';
|
||
}).catch(() => {
|
||
document.getElementById('country-' + id).textContent = 'Erro';
|
||
document.getElementById('dot-' + id).style.background = '#6e2120';
|
||
});
|
||
}
|
||
|
||
function loadWG() {
|
||
fetch('/vpn.php?ajax=wg').then(r => r.json()).then(d => {
|
||
const ifaces = d.ifaces || {};
|
||
['wg0','wg1'].forEach(name => {
|
||
const wg = ifaces[name] || {peers:0, active:0};
|
||
const info = document.getElementById(name + '-info');
|
||
const dot = document.getElementById('dot-' + name);
|
||
if (info) info.textContent = (wg.ext || '—') + ' · ' + wg.peers + ' peers · ' + wg.active + ' activo' + (wg.active !== 1 ? 's' : '');
|
||
if (dot) dot.style.background = wg.active > 0 ? '#3fb950' : '#7d8590';
|
||
});
|
||
}).catch(() => {
|
||
['wg0','wg1'].forEach(name => {
|
||
const info = document.getElementById(name + '-info');
|
||
const dot = document.getElementById('dot-' + name);
|
||
if (info) info.textContent = 'Erro SSH';
|
||
if (dot) dot.style.background = '#6e2120';
|
||
});
|
||
});
|
||
}
|
||
|
||
<?php foreach (array_keys($NORDVPNS) as $id): ?>loadNord('<?= $id ?>');<?php endforeach; ?>
|
||
loadWG();
|
||
</script>
|
||
|
||
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';
|