commit-all-changes

This commit is contained in:
deploy 2026-06-27 21:44:44 +00:00
parent 0d8cf8a7e5
commit bea0c5c200
24 changed files with 2318 additions and 1916 deletions

View File

@ -1,35 +1,35 @@
<?php
declare(strict_types=1);
require __DIR__ . "/db.php";
$rows = $pdo->query("
SELECT uid, note, entradas, saidas, online, blocked,
current_broker, rssi, heap, uptime,
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
FROM contadores
ORDER BY online DESC, uid ASC
")->fetchAll();
// Totais
$sumEntradas = 0;
$sumSaidas = 0;
$onlineCount = 0;
$offlineCount = 0;
foreach ($rows as $r) {
$sumEntradas += (int)$r['entradas'];
$sumSaidas += (int)$r['saidas'];
if ($r['online'] == 1) $onlineCount++;
else $offlineCount++;
}
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: 0");
echo json_encode([
"rows" => $rows,
"online" => $onlineCount,
"offline" => $offlineCount,
"entradas" => $sumEntradas,
"saidas" => $sumSaidas,
]);
<?php
declare(strict_types=1);
require __DIR__ . "/db.php";
$rows = $pdo->query("
SELECT uid, note, entradas, saidas, online, blocked,
current_broker, rssi, heap, uptime,
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
FROM contadores
ORDER BY online DESC, uid ASC
")->fetchAll();
// Totais
$sumEntradas = 0;
$sumSaidas = 0;
$onlineCount = 0;
$offlineCount = 0;
foreach ($rows as $r) {
$sumEntradas += (int)$r['entradas'];
$sumSaidas += (int)$r['saidas'];
if ($r['online'] == 1) $onlineCount++;
else $offlineCount++;
}
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: 0");
echo json_encode([
"rows" => $rows,
"online" => $onlineCount,
"offline" => $offlineCount,
"entradas" => $sumEntradas,
"saidas" => $sumSaidas,
]);

View File

@ -50,29 +50,12 @@ $db = new PDO(
]
);
// =====================================================
// RECOVERY MQTT
// =====================================================
$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
$recoveryPort = 8884;
$recoveryUser = 'recovery';
$recoveryPass = 'recovery123';
// =====================================================
// MAIN MQTT
// =====================================================
$mainHost = 'mqtt.xupas.mywire.org';
$mainPort = 8883;
// =====================================================
// CERT
// =====================================================
$certPath = '/etc/mosquitto/certs/chain.pem'; // cert Let's Encrypt do broker principal
$certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
// ─── migração: coluna auth_nonce_at (idempotente) ──────
$db->exec("
ALTER TABLE contadores
ADD COLUMN IF NOT EXISTS
auth_nonce_at DATETIME NULL DEFAULT NULL
");
// =====================================================
// GLOBAL SECRET
@ -81,28 +64,92 @@ $certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
$authSecret = 'K82A91F0';
// =====================================================
// MQTT SETTINGS
// CA CERT mqtt-recovery.xpto.ooguy.com (self-signed)
// =====================================================
$settings = (new ConnectionSettings())
->setUsername($recoveryUser)
->setPassword($recoveryPass)
->setUseTls(true)
->setTlsSelfSignedAllowed(true)
->setKeepAliveInterval(30)
->setConnectTimeout(5)
->setSocketTimeout(1)
->setReconnectAutomatically(true);
$recoveryXptoCaCert = <<<'EOT'
-----BEGIN CERTIFICATE-----
MIIDuTCCAqGgAwIBAgIUass5a34ekbqg0iaygCtiGRiSFHIwDQYJKoZIhvcNAQEL
BQAwbDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDElMCMGA1UEAwwcbXF0dC1yZWNvdmVy
eS54cHRvLm9vZ3V5LmNvbTAeFw0yNjA1MjYxMjM1NDFaFw0zNjA1MjMxMjM1NDFa
MGwxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ
bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxJTAjBgNVBAMMHG1xdHQtcmVjb3Zlcnku
eHB0by5vb2d1eS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDe
2uCWyHP/xng8DlL4SgqlXq9YbZQw/XWIoNDcpCPAirSKW/d6RNoIa4/UoUtOVfUq
vN7JgAi+Ovz86q+aagYdVgtU/pIsj/U3/uwZ8Ga2g/AWCD2WZawE/SfpIoape6PM
EnvSjFpyTZCLcOh6xUIdqH2/RcI7WJvgBBFkocZuP12zJD4jAIZC41+LpjUQQ6rM
DANhmUYD2fVXdQ8f9hWuKH0inal+2eHtY6n4oL3RZdaSQNQz0j5QKeVfMuAKrMNu
JeUEYku7Exvr6slaeoEVqlj7UwfwCPNernOvleB5y9dMKDNbXeHAFBdrPMQrgAK7
KZvNq/5caqpzdxZfskJFAgMBAAGjUzBRMB0GA1UdDgQWBBThMl6pM+mkvCeVFwkX
NTTnE0R2jDAfBgNVHSMEGDAWgBThMl6pM+mkvCeVFwkXNTTnE0R2jDAPBgNVHRMB
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDDSbFkl1BIotX6dREzyYh0H4dF
NTU3zAayZnd4k33oIgpVnpoVd9PBwzRiRc8PuTiQv6iF2vsmJY/xghNp7UlgHo4k
UEuF15F24fCj9Xqpbm1EgDGcDzwOsS+RD91bIXjt5U4m8S1meoJ9+xxK6LanUwp8
FSgxiw+NGcaDMH4Dc6U6Qvxo3ycIkaJQ+SuK72RW0naC9oMp+Yq0otUjDpUkkmP9
fNex9ArIrKPhWqWBRnImVStfY/1nzg9Eh7bS6k+qpVIEfvKVLdt2WfWDDtazaJpq
eJSAR5CXLy3G8OKZpcSe0YXb2NkDX/R88B7z9uGD6Zz/lHE9Zot+9JS6tcvZ
-----END CERTIFICATE-----
EOT;
$recoveryXptoCaFile = '/etc/mosquitto/certs/recovery-xpto-ca.pem';
if (
!file_exists($recoveryXptoCaFile) ||
trim((string) file_get_contents($recoveryXptoCaFile)) !== trim($recoveryXptoCaCert)
) {
file_put_contents($recoveryXptoCaFile, $recoveryXptoCaCert);
logmsg("CA cert escrito: $recoveryXptoCaFile");
}
// =====================================================
// MQTT CLIENT
// BROKER PAIRS
// =====================================================
//
// Cada par agrupa:
// recvHost/Port/User/Pass → recovery broker (onde o ESP se liga quando o principal cai)
// recvCaFile → CA para verificar o cert TLS do recovery broker
// null = usar CA do sistema (Let's Encrypt)
// selfSigned → true = aceitar certs self-signed (TlsSelfSignedAllowed)
// mainHost/Port → main broker (destino do SWITCH_PRIMARY)
// certPath → cert do main broker guardado localmente (enviado ao ESP)
// certUrl → URL para refrescar o cert do main broker
//
// =====================================================
$mqtt = new MqttClient(
$recoveryHost,
$recoveryPort,
'listener-recovery-' . getmypid()
);
$brokerPairs = [
// ── Remoto — Let's Encrypt ─────────────────────
[
'label' => 'remote',
'recvHost' => 'mqtt-recovery.xupas.mywire.org',
'recvPort' => 8884,
'recvUser' => 'recovery',
'recvPass' => 'recovery123',
'recvCaFile' => null, // Let's Encrypt — CA do sistema
'selfSigned' => true, // mantém comportamento original
'mainHost' => 'mqtt.xupas.mywire.org',
'mainPort' => 8883,
'certPath' => '/etc/mosquitto/certs/device-ca.pem',
'certUrl' => 'https://mqtt.xupas.mywire.org/cert.pem',
],
// ── Local — self-signed ────────────────────────
[
'label' => 'local-selfsigned',
'recvHost' => 'mqtt-recovery.xpto.ooguy.com',
'recvPort' => 8884,
'recvUser' => 'xupa',
'recvPass' => 'xupa',
'recvCaFile' => $recoveryXptoCaFile,
'selfSigned' => true,
'mainHost' => 'mqtt.xpto.ooguy.com',
'mainPort' => 8883,
'certPath' => '/etc/mosquitto/certs/device-ca.pem',
'certUrl' => 'https://mqtt.xpto.ooguy.com/cert.pem',
],
];
// =====================================================
// MAIN BROKER CHECK
@ -206,18 +253,41 @@ function send_cert_chunks(
}
// =====================================================
// SUBSCRIPTIONS
// MAKE SETTINGS
// =====================================================
$subscribe = function () use (
$mqtt,
$db,
$certPath,
$certUrl,
$mainHost,
$mainPort,
$authSecret
) {
function make_settings(array $pair): ConnectionSettings
{
$s = (new ConnectionSettings())
->setUsername($pair['recvUser'])
->setPassword($pair['recvPass'])
->setUseTls(true)
->setTlsSelfSignedAllowed($pair['selfSigned'])
->setKeepAliveInterval(30)
->setConnectTimeout(5)
->setSocketTimeout(1)
->setReconnectAutomatically(true);
if ($pair['recvCaFile'] !== null) {
$s = $s->setTlsCertificateAuthorityFile($pair['recvCaFile']);
}
return $s;
}
// =====================================================
// SUBSCRIBE BROKER
// =====================================================
function subscribe_broker(
MqttClient $mqtt,
PDO $db,
array $pair,
string $authSecret
): void {
$label = $pair['label'];
$certPath = $pair['certPath'];
// =================================================
// AUTH
@ -226,7 +296,7 @@ $subscribe = function () use (
$mqtt->subscribe(
'esp/+/auth',
function (string $topic, string $msg)
use ($db, $mqtt, $authSecret)
use ($db, $mqtt, $authSecret, $label)
{
$uid = explode('/', $topic)[1] ?? null;
@ -235,17 +305,12 @@ $subscribe = function () use (
return;
}
logmsg("AUTH RX $uid => $msg");
logmsg("[$label] AUTH RX $uid => $msg");
$json = json_decode($msg, true);
if (!$json) {
logmsg(
"JSON AUTH inválido",
LOG_ERROR
);
logmsg("[$label] JSON AUTH inválido", LOG_ERROR);
return;
}
@ -255,41 +320,62 @@ $subscribe = function () use (
if (($json['cmd'] ?? '') === 'HELLO') {
$nonce =
strtoupper(
bin2hex(random_bytes(16))
);
// ─── Idempotência: evitar AUTH_CHALLENGE duplicado ─
// Quando o listener reinicia, o broker pode reentrogar
// mensagens QoS=1 não ACKadas pela instância anterior.
// Se já existe um nonce recente (< 30 s) reutilizamo-lo
// em vez de gerar um novo e sobrescrever o DB.
$stmt = $db->prepare("
INSERT INTO contadores
(uid, auth_nonce, last_seen, online, blocked)
VALUES
(:uid, :nonce, NOW(), 1, 1)
ON DUPLICATE KEY UPDATE
auth_nonce=:nonce,
last_seen=NOW(),
online=1
SELECT auth_nonce, auth_nonce_at
FROM contadores
WHERE uid=:uid
LIMIT 1
");
/* novos ESPs entram com blocked=1 (pendente aprovação)
ESPs existentes mantêm o blocked actual */
$stmt->execute(['uid' => $uid]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt->execute([
'uid' => $uid,
'nonce' => $nonce
]);
$existingNonce = $existing['auth_nonce'] ?? null;
$nonceAge = $existing['auth_nonce_at']
? (time() - strtotime($existing['auth_nonce_at']))
: PHP_INT_MAX;
if ($existingNonce && $nonceAge < 30) {
// Nonce recente — reutilizar sem tocar no DB
$nonce = $existingNonce;
logmsg("[$label] AUTH_CHALLENGE reutilizado para $uid (age {$nonceAge}s)");
} else {
$nonce = strtoupper(bin2hex(random_bytes(16)));
$stmt = $db->prepare("
INSERT INTO contadores
(uid, auth_nonce, auth_nonce_at, last_seen, online, blocked)
VALUES
(:uid, :nonce, NOW(), NOW(), 1, 1)
ON DUPLICATE KEY UPDATE
auth_nonce=:nonce,
auth_nonce_at=NOW(),
last_seen=NOW(),
online=1
");
/* novos ESPs entram com blocked=1 (pendente aprovação)
ESPs existentes mantêm o blocked actual */
$stmt->execute([
'uid' => $uid,
'nonce' => $nonce,
]);
}
$mqtt->publish(
"esp/$uid/cmd",
json_encode([
'cmd' => 'AUTH_CHALLENGE',
'nonce' => $nonce
'cmd' => 'AUTH_CHALLENGE',
'nonce' => $nonce,
]),
1
);
logmsg(
"AUTH_CHALLENGE enviado para $uid"
);
logmsg("[$label] AUTH_CHALLENGE enviado para $uid");
return;
}
@ -306,12 +392,7 @@ $subscribe = function () use (
);
if (!$hash) {
logmsg(
"AUTH sem hash",
LOG_ERROR
);
logmsg("[$label] AUTH sem hash", LOG_ERROR);
return;
}
@ -326,29 +407,17 @@ $subscribe = function () use (
'uid' => $uid
]);
$row =
$stmt->fetch(PDO::FETCH_ASSOC);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
logmsg(
"UID desconhecido: $uid",
LOG_ERROR
);
logmsg("[$label] UID desconhecido: $uid", LOG_ERROR);
return;
}
$nonce =
$row['auth_nonce'] ?? '';
$nonce = $row['auth_nonce'] ?? '';
if (!$nonce) {
logmsg(
"Nonce vazio para $uid",
LOG_ERROR
);
logmsg("[$label] Nonce vazio para $uid", LOG_ERROR);
return;
}
@ -366,7 +435,10 @@ $subscribe = function () use (
// verificar se está aprovado
if ((int)($row['blocked'] ?? 1) === 1) {
logmsg("AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação", LOG_ERROR);
logmsg(
"[$label] AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação",
LOG_ERROR
);
$mqtt->publish(
"esp/$uid/cmd",
@ -377,12 +449,13 @@ $subscribe = function () use (
return;
}
logmsg("AUTH OK $uid");
logmsg("[$label] AUTH OK $uid");
$stmt = $db->prepare("
UPDATE contadores
SET
auth_nonce=NULL,
auth_nonce_at=NULL,
auth_ok=1,
online=1,
last_seen=NOW()
@ -403,18 +476,10 @@ $subscribe = function () use (
} else {
logmsg(
"AUTH FAIL $uid",
LOG_ERROR
);
$mqtt->publish(
"esp/$uid/cmd",
json_encode([
'cmd' => 'AUTH_FAIL'
]),
1
);
// Hash nao bate: quase sempre e cross-talk - o device respondeu
// ao nonce do OUTRO listener (VPS). NAO mandar AUTH_FAIL (causava
// boot-loop). So o listener cujo nonce bate manda AUTH_OK.
logmsg("[$label] AUTH hash mismatch $uid - ignorado (cross-talk)", LOG_ERROR);
}
return;
@ -433,9 +498,7 @@ $subscribe = function () use (
string $msg
) use (
$db,
$mqtt,
$mainHost,
$mainPort
$label
) {
$uid = explode('/', $topic)[1] ?? null;
@ -474,41 +537,7 @@ $subscribe = function () use (
'uid' => $uid
]);
logmsg(
"RECOVERY STATUS $uid => $msg"
);
static $lastCheck = 0;
if (time() - $lastCheck >= 15) {
$lastCheck = time();
logmsg("CHECK broker");
if (
main_broker_online(
$mainHost,
$mainPort
)
) {
logmsg(
"MAIN broker online"
);
send_switch_primary(
$mqtt,
$db
);
} else {
logmsg(
"MAIN broker offline"
);
}
}
logmsg("[$label] RECOVERY STATUS $uid => $msg");
},
0
@ -523,18 +552,15 @@ $subscribe = function () use (
function (
string $topic,
string $msg
) {
) use ($label) {
$uid =
explode('/', $topic)[1] ?? null;
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) {
return;
}
logmsg(
"RECOVERY EVENT $uid => $msg"
);
logmsg("[$label] RECOVERY EVENT $uid => $msg");
},
0
@ -552,100 +578,165 @@ $subscribe = function () use (
) use (
$mqtt,
$certPath,
$certUrl
$label
) {
$uid =
explode('/', $topic)[1] ?? null;
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) {
return;
}
logmsg("CERT_REQUEST de $uid → chunks");
logmsg("[$label] CERT_REQUEST de $uid → chunks");
send_cert_chunks($mqtt, $uid, $certPath);
},
1
);
};
}
// =====================================================
// MAIN LOOP
// =====================================================
$clients = []; // label => MqttClient
$nextCheck = []; // label => timestamp da próxima verificação do main broker
$lastConnectAttempt = []; // label => timestamp da última tentativa de ligação
while (true) {
try {
// ── connect / reconnect ────────────────────────
logmsg(
"Ligar recovery $recoveryHost:$recoveryPort"
);
foreach ($brokerPairs as $pair) {
$mqtt->connect($settings, false);
$subscribe();
logmsg(
"RECOVERY ligado e subscrito"
);
$nextMainCheck = time() + 10;
$loopStartedAt = microtime(true);
while ($mqtt->isConnected()) {
$mqtt->loopOnce($loopStartedAt, true, 100000);
if (time() >= $nextMainCheck) {
$nextMainCheck = time() + 15;
logmsg(
"LOOP recovery",
LOG_DEBUG
);
if (
main_broker_online(
$mainHost,
$mainPort
)
) {
logmsg(
"MAIN broker online"
);
logmsg("Mandar switch_primary");
send_switch_primary(
$mqtt,
$db
);
} else {
logmsg(
"MAIN broker ainda offline",
LOG_DEBUG
);
}
}
$label = $pair['label'];
if (
isset($clients[$label]) &&
$clients[$label]->isConnected()
) {
continue;
}
$mqtt->disconnect();
// throttle: não tentar mais do que 1x / 5 s por broker
if (time() < ($lastConnectAttempt[$label] ?? 0) + 5) {
continue;
}
} catch (Throwable $e) {
$lastConnectAttempt[$label] = time();
logmsg(
"ERRO recovery: " .
$e->getMessage(),
LOG_ERROR
);
try {
sleep(5);
logmsg(
"Ligar recovery [{$label}] " .
"{$pair['recvHost']}:{$pair['recvPort']}"
);
$clients[$label] = new MqttClient(
$pair['recvHost'],
$pair['recvPort'],
'listener-recovery-' . $label . '-' . getmypid()
);
$clients[$label]->connect(
make_settings($pair),
false
);
subscribe_broker(
$clients[$label],
$db,
$pair,
$authSecret
);
// primeira verificação do main broker após 10 s
$nextCheck[$label] = time() + 10;
logmsg("[{$label}] ligado e subscrito");
} catch (Throwable $e) {
logmsg(
"[{$label}] ERRO connect: " . $e->getMessage(),
LOG_ERROR
);
unset($clients[$label]);
}
}
}
if (empty($clients)) {
sleep(5);
continue;
}
// ── drive all clients (50 ms cada) ────────────
$loopStartedAt = microtime(true);
foreach ($clients as $label => $mqtt) {
try {
$mqtt->loopOnce($loopStartedAt, true, 50000);
} catch (Throwable $e) {
logmsg(
"[{$label}] ERRO loop: " . $e->getMessage(),
LOG_ERROR
);
try {
$mqtt->disconnect();
} catch (Throwable $_) {}
unset($clients[$label]);
}
}
// ── periodic main broker check per pair ────────
$now = time();
foreach ($brokerPairs as $pair) {
$label = $pair['label'];
if (
!isset($clients[$label]) ||
!$clients[$label]->isConnected()
) {
continue;
}
if ($now < ($nextCheck[$label] ?? 0)) {
continue;
}
$nextCheck[$label] = $now + 15;
logmsg("[{$label}] CHECK main broker", LOG_DEBUG);
if (
main_broker_online(
$pair['mainHost'],
$pair['mainPort']
)
) {
logmsg("[{$label}] MAIN broker online → SWITCH_PRIMARY");
send_switch_primary($clients[$label], $db);
} else {
logmsg(
"[{$label}] MAIN broker ainda offline",
LOG_DEBUG
);
}
}
}

View File

@ -1,163 +1,3 @@
<?php
require_once "protecao.php";
/* =========================
CONTROLO DA VPN
========================= */
$outputVPN = null;
if (isset($_GET['vpn'])) {
$allowed = ['md','al','nl','de','ch','status'];
$cmd = $_GET['vpn'];
if (in_array($cmd, $allowed)) {
// IP do LXC NordVPN (104)
$vpnHost = "192.168.10.104";
$vpnUser = "root";
$sshCmd = "/usr/bin/ssh -i /var/www/.ssh/nordvpn "
. "-o BatchMode=yes -o StrictHostKeyChecking=no "
. "{$vpnUser}@{$vpnHost} "
. escapeshellarg($cmd)
. " 2>&1";
$outputVPN = shell_exec($sshCmd);
} else {
$outputVPN = "❌ Comando VPN inválido.";
}
}
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Menu Principal</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #0f0f0f;
color: #e0e0e0;
font-family: 'Segoe UI', sans-serif;
}
h2, h4 {
color: #00ffc8;
text-shadow: 0 0 6px #00ffc8;
}
.btn {
border-width: 2px;
font-weight: 600;
padding: 8px 14px;
font-size: 0.9rem;
transition: all 0.15s ease-in-out;
}
.btn:hover {
box-shadow: 0 0 8px currentColor;
}
pre {
background: #000;
color: #00ff99;
padding: 12px;
border-radius: 6px;
font-size: 0.85rem;
text-align: left;
}
.container {
max-width: 720px;
}
footer {
margin-top: 50px;
font-size: 0.85rem;
color: #777;
}
</style>
</head>
<body class="p-4">
<div class="container text-center">
<h2 class="mb-4">
Bem-vindo, <?= htmlspecialchars($_SESSION['username']) ?> 😎
</h2>
<!-- MENU PRINCIPAL -->
<div class="row g-2 mb-5">
<div class="col-12">
<a href="../console.php" class="btn btn-outline-light w-100">📊 Estado dos Dispositivos</a>
</div>
<div class="col-12">
<a href="../devices.php" class="btn btn-outline-warning w-100">🔌 Gerir Dispositivos</a>
</div>
<div class="col-12">
<a href="../comandos.php" class="btn btn-outline-light w-100">🛠️ Enviar Comando</a>
</div>
<div class="col-12">
<a href="../historico.php" class="btn btn-outline-light w-100">📜 Ver Histórico</a>
</div>
<div class="col-12">
<a href="config.php" class="btn btn-outline-info w-100">⚙️ Configurações</a>
</div>
<div class="col-12">
<a href="https://web.xupas.mywire.org/matrix_admin.php" target="_blank"
class="btn btn-outline-success w-100">
💬 Matrix Admin (Web)
</a>
</div>
<div class="col-12">
<a href="https://admmatrix.xupas.mywire.org" target="_blank"
class="btn btn-outline-warning w-100">
🧠 Synapse Admin (Servidor)
</a>
</div>
</div>
<!-- CONTROLO DA VPN -->
<hr class="my-4">
<h4 class="mb-3">🌍 Controlo da VPN</h4>
<div class="row g-2 mb-4">
<div class="col-12 col-md-6">
<a href="?vpn=md" class="btn btn-outline-success w-100">🇲🇩 Moldova (YouTube)</a>
</div>
<div class="col-12 col-md-6">
<a href="?vpn=al" class="btn btn-outline-success w-100">🇦🇱 Albania (Ad-free)</a>
</div>
<div class="col-12 col-md-6">
<a href="?vpn=nl" class="btn btn-outline-info w-100">🇳🇱 Netherlands (Torrent)</a>
</div>
<div class="col-12 col-md-6">
<a href="?vpn=de" class="btn btn-outline-info w-100">🇩🇪 Germany (Speed)</a>
</div>
<div class="col-12">
<a href="?vpn=status" class="btn btn-outline-light w-100">📡 Ver Estado VPN</a>
</div>
</div>
<?php if ($outputVPN): ?>
<div class="mb-4">
<pre><?= htmlspecialchars($outputVPN) ?></pre>
</div>
<?php endif; ?>
<!-- LOGOUT -->
<div class="row">
<div class="col-12">
<a href="logout.php" class="btn btn-outline-danger w-100">🚪 Terminar Sessão</a>
</div>
</div>
<footer class="text-center mt-5">
<p>© <?= date('Y') ?> Xupas Systems | Todos os direitos reservados</p>
</footer>
</div>
</body>
</html>
header("Location: /vpn.php");
exit;

View File

@ -1,11 +1,11 @@
<?php
declare(strict_types=1);
function mqtt_publish(string $topic, string $msg): void {
$cmd = sprintf(
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
escapeshellarg($topic),
escapeshellarg($msg)
);
shell_exec($cmd . " > /dev/null 2>&1");
}
<?php
declare(strict_types=1);
function mqtt_publish(string $topic, string $msg): void {
$cmd = sprintf(
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
escapeshellarg($topic),
escapeshellarg($msg)
);
shell_exec($cmd . " > /dev/null 2>&1");
}

View File

@ -1,44 +1,44 @@
<?php
declare(strict_types=1);
/**
* Reset de parciais com acumulação em totais (por máquina):
* total += parcial; parcial = 0
*/
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED & ~E_NOTICE & ~E_USER_NOTICE);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
try {
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// garantir colunas (ignora se já existirem)
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
$uid = $_POST['uid'] ?? '';
if ($uid === '') {
header("Location: /console.php?ok=err");
exit;
}
$stmt = $db->prepare("
UPDATE contadores
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
entradas = 0,
saidas = 0
WHERE uid = :uid
");
$stmt->execute(['uid' => $uid]);
header("Location: /console.php?ok=line");
exit;
} catch (Throwable $e) {
header("Location: /console.php?ok=err");
exit;
}
<?php
declare(strict_types=1);
/**
* Reset de parciais com acumulação em totais (por máquina):
* total += parcial; parcial = 0
*/
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED & ~E_NOTICE & ~E_USER_NOTICE);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
try {
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// garantir colunas (ignora se já existirem)
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
$uid = $_POST['uid'] ?? '';
if ($uid === '') {
header("Location: /console.php?ok=err");
exit;
}
$stmt = $db->prepare("
UPDATE contadores
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
entradas = 0,
saidas = 0
WHERE uid = :uid
");
$stmt->execute(['uid' => $uid]);
header("Location: /console.php?ok=line");
exit;
} catch (Throwable $e) {
header("Location: /console.php?ok=err");
exit;
}

View File

@ -1,11 +1,11 @@
<?php
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
$stmt->execute([$newPassword]);
echo "✅ Password de admin alterada para admin123";
<?php
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
$stmt->execute([$newPassword]);
echo "✅ Password de admin alterada para admin123";

View File

@ -1,34 +1,34 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require __DIR__ . '/db.php'; // ligação PDO
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
http_response_code(400);
echo json_encode(['error'=>'invalid_request']);
exit;
}
$uid = $data['uid'];
$cmd = $data['cmd'];
$value = $data['value'] ?? null;
// ----------------------- MQTT -----------------------
$topic = "esp/$uid/cmd";
$msg = ['cmd' => $cmd];
if ($value !== null) $msg['value'] = $value;
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
// ----------------------- BD -------------------------
if ($cmd === 'BLOCK') {
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
$stmt->execute([intval($value), $uid]);
}
echo json_encode(['ok'=>1]);
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require __DIR__ . '/db.php'; // ligação PDO
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
http_response_code(400);
echo json_encode(['error'=>'invalid_request']);
exit;
}
$uid = $data['uid'];
$cmd = $data['cmd'];
$value = $data['value'] ?? null;
// ----------------------- MQTT -----------------------
$topic = "esp/$uid/cmd";
$msg = ['cmd' => $cmd];
if ($value !== null) $msg['value'] = $value;
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
// ----------------------- BD -------------------------
if ($cmd === 'BLOCK') {
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
$stmt->execute([intval($value), $uid]);
}
echo json_encode(['ok'=>1]);

View File

@ -1,23 +1,23 @@
<?php
set_time_limit(30);
require __DIR__ . '/vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
$data = [];
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
$mqtt->connect($settings, true);
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
$data[] = ['topic'=>$topic, 'message'=>$message];
if (count($data) >= 5) {
$mqtt->interrupt(); // para o loop depois de 5 msg
}
}, 0);
$mqtt->loop(true);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT);
<?php
set_time_limit(30);
require __DIR__ . '/vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
$data = [];
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
$mqtt->connect($settings, true);
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
$data[] = ['topic'=>$topic, 'message'=>$message];
if (count($data) >= 5) {
$mqtt->interrupt(); // para o loop depois de 5 msg
}
}, 0);
$mqtt->loop(true);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT);

View File

@ -33,7 +33,7 @@ try {
$_SESSION['username'] = $user['username'];
// Redireciona para o menu principal
header("Location: /api/menu.php");
header("Location: /painel.php");
exit;
} catch (Exception $e) {

View File

@ -1,25 +1,25 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"

View File

@ -1,55 +1,55 @@
<?php
// ---------------- DB ----------------
$dbHost = "localhost";
$dbName = "cagalhao";
$dbUser = "master";
$dbPass = "master";
try {
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Erro DB: " . $e->getMessage());
}
$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Histórico de Comandos</title>
<style>
body { font-family: Arial, sans-serif; background: #111; color: #eee; }
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
th, td { border: 1px solid #444; padding: 6px 10px; text-align: center; }
th { background: #222; }
tr:nth-child(even) { background: #1b1b1b; }
</style>
</head>
<body>
<h1>Histórico de Comandos</h1>
<table>
<tr>
<th>ID</th>
<th>UID</th>
<th>CMD</th>
<th>Params</th>
<th>Status</th>
<th>Data</th>
</tr>
<?php foreach ($comandos as $cmd): ?>
<tr>
<td><?= $cmd['id'] ?></td>
<td><?= htmlspecialchars($cmd['uid']) ?></td>
<td><?= htmlspecialchars($cmd['cmd']) ?></td>
<td><pre><?= htmlspecialchars($cmd['params']) ?></pre></td>
<td><?= $cmd['status'] ?></td>
<td><?= $cmd['created_at'] ?? '' ?></td>
</tr>
<?php endforeach; ?>
</table>
<p><a href="/api/menu.php">⬅️ Voltar</a></p>
</body>
</html>
<?php
// ---------------- DB ----------------
$dbHost = "localhost";
$dbName = "cagalhao";
$dbUser = "master";
$dbPass = "master";
try {
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Erro DB: " . $e->getMessage());
}
$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Histórico de Comandos</title>
<style>
body { font-family: Arial, sans-serif; background: #111; color: #eee; }
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
th, td { border: 1px solid #444; padding: 6px 10px; text-align: center; }
th { background: #222; }
tr:nth-child(even) { background: #1b1b1b; }
</style>
</head>
<body>
<h1>Histórico de Comandos</h1>
<table>
<tr>
<th>ID</th>
<th>UID</th>
<th>CMD</th>
<th>Params</th>
<th>Status</th>
<th>Data</th>
</tr>
<?php foreach ($comandos as $cmd): ?>
<tr>
<td><?= $cmd['id'] ?></td>
<td><?= htmlspecialchars($cmd['uid']) ?></td>
<td><?= htmlspecialchars($cmd['cmd']) ?></td>
<td><pre><?= htmlspecialchars($cmd['params']) ?></pre></td>
<td><?= $cmd['status'] ?></td>
<td><?= $cmd['created_at'] ?? '' ?></td>
</tr>
<?php endforeach; ?>
</table>
<p><a href="/api/menu.php">⬅️ Voltar</a></p>
</body>
</html>

View File

@ -1,11 +1,11 @@
<?php
return [
'host' => 'mqtt.xupas.mywire.org',
'port' => 8883,
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
'username' => 'xupa',
'password' => 'xupa',
'clean' => true,
'keepalive'=> 60,
'timeout' => 5,
];
<?php
return [
'host' => 'mqtt.xupas.mywire.org',
'port' => 8883,
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
'username' => 'xupa',
'password' => 'xupa',
'clean' => true,
'keepalive'=> 60,
'timeout' => 5,
];

File diff suppressed because it is too large Load Diff

View File

@ -122,6 +122,7 @@ $pending = $pdo->query("
$approved = $pdo->query("
SELECT uid, note, last_seen, online, blocked, auth_ok, current_broker,
COALESCE(brightness, 128) AS brightness,
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
FROM contadores
WHERE blocked=0 OR auth_ok=1
@ -208,6 +209,7 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
<th>ESTADO</th>
<th>BROKER</th>
<th>ÚLTIMO</th>
<th>BRILHO</th>
<th>AÇÕES</th>
</tr>
</thead>
@ -231,12 +233,27 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
? '<span class="badge bg-warning text-dark">RECOVERY</span>'
: '<span class="badge bg-primary">PRIMARY</span>';
?>
<?php $bri = (int)($r['brightness'] ?? 128); ?>
<tr>
<td class="fw-bold"><?= htmlspecialchars($r['uid']) ?></td>
<td><?= htmlspecialchars($r['note'] ?? '') ?></td>
<td><?= $state ?></td>
<td><?= $broker ?></td>
<td><?= $lastSeen ?></td>
<td>
<div class="d-flex align-items-center gap-2">
<input type="range" class="form-range" min="0" max="255"
value="<?= $bri ?>"
id="bri_<?= htmlspecialchars($r['uid']) ?>"
style="width:110px"
oninput="setBri('<?= htmlspecialchars($r['uid']) ?>',this.value)"
<?= $online ? '' : 'disabled' ?>>
<span id="brival_<?= htmlspecialchars($r['uid']) ?>"
class="text-muted" style="min-width:28px;font-size:.85em">
<?= $bri ?>
</span>
</div>
</td>
<td>
<div class="d-flex gap-2">
<a href="?toggle=<?= urlencode($r['uid']) ?>"
@ -262,5 +279,20 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
</div>
</div>
<script>
const briTimers = {};
function setBri(uid, val) {
document.getElementById('brival_' + uid).textContent = val;
clearTimeout(briTimers[uid]);
briTimers[uid] = setTimeout(() => {
const body = new URLSearchParams({ uid, value: val });
fetch('/api/set_brightness.php', { method: 'POST', body })
.then(r => r.json())
.then(d => { if (!d.ok) console.warn('brightness err', d); })
.catch(e => console.error(e));
}, 350);
}
</script>
</body>
</html>

View File

@ -1,33 +1,33 @@
<?php
declare(strict_types=1);
header("Content-Type: application/json; charset=utf-8");
ini_set('display_errors', '1');
error_reporting(E_ALL);
try {
$cfg = @include __DIR__ . "/config_db.php";
if (!is_array($cfg)) {
throw new RuntimeException("Config DB inválida ou em falta (config_db.php).");
}
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", $cfg['db_host'], $cfg['db_name']);
$pdo = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare("SELECT token_plain FROM api_tokens WHERE label = ? AND revoked = 0 ORDER BY id DESC LIMIT 1");
$stmt->execute(['matrix-admin']);
$row = $stmt->fetch();
if (!$row || empty($row['token_plain'])) {
http_response_code(404);
echo json_encode(["error" => "Token não encontrado"]);
exit;
}
echo json_encode(["token" => $row["token_plain"]]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
}
<?php
declare(strict_types=1);
header("Content-Type: application/json; charset=utf-8");
ini_set('display_errors', '1');
error_reporting(E_ALL);
try {
$cfg = @include __DIR__ . "/config_db.php";
if (!is_array($cfg)) {
throw new RuntimeException("Config DB inválida ou em falta (config_db.php).");
}
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", $cfg['db_host'], $cfg['db_name']);
$pdo = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare("SELECT token_plain FROM api_tokens WHERE label = ? AND revoked = 0 ORDER BY id DESC LIMIT 1");
$stmt->execute(['matrix-admin']);
$row = $stmt->fetch();
if (!$row || empty($row['token_plain'])) {
http_response_code(404);
echo json_encode(["error" => "Token não encontrado"]);
exit;
}
echo json_encode(["token" => $row["token_plain"]]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
}

View File

@ -1,156 +1,156 @@
<?php
require_once __DIR__ . "/api/protecao.php";
try {
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Se pediu para limpar pendentes
if (isset($_POST['clear_pending'])) {
$pdo->exec("DELETE FROM comandos WHERE status = 'pending'");
$_SESSION['flash'] = "🧹 Todos os comandos pendentes foram removidos!";
header("Location: historico.php");
exit;
}
// Filtros
$where = [];
$params = [];
if (!empty($_GET['uid'])) {
$where[] = "c.uid = ?";
$params[] = $_GET['uid'];
}
if (!empty($_GET['status'])) {
$where[] = "c.status = ?";
$params[] = $_GET['status'];
}
$sql = "SELECT c.id, c.uid, c.cmd, c.status, c.queued_at, c.sent_at, c.ack_ts, c.error,
r.payload AS resposta, r.ts AS resposta_at
FROM comandos c
LEFT JOIN respostas r ON r.uid = c.uid
" . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
ORDER BY c.queued_at DESC
LIMIT 50";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$comandos = $stmt->fetchAll();
$devices = $pdo->query("SELECT DISTINCT uid FROM devices ORDER BY uid ASC")->fetchAll();
} catch (Exception $e) {
die("Erro: " . $e->getMessage());
}
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Histórico de Comandos</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #121212; color: #eee; }
.card, .table { background-color: #1e1e1e; color: #ddd; }
.table thead { background-color: #333; }
</style>
<script>
// Auto-refresh a cada 5 segundos
setTimeout(() => location.reload(), 5000);
</script>
</head>
<body class="p-4">
<div class="container-fluid">
<h2 class="mb-4 text-info">📜 Histórico de Comandos</h2>
<?php if (!empty($_SESSION['flash'])): ?>
<div class="alert alert-success"><?= $_SESSION['flash']; unset($_SESSION['flash']); ?></div>
<?php endif; ?>
<!-- Botão para limpar pendentes -->
<form method="POST" class="mb-3">
<button type="submit" name="clear_pending" class="btn btn-danger">
🧹 Limpar Pendentes
</button>
</form>
<!-- Filtros -->
<form method="GET" class="row g-3 mb-4">
<div class="col-md-4">
<select name="uid" class="form-select">
<option value="">-- Todos os dispositivos --</option>
<?php foreach ($devices as $d): ?>
<option value="<?= htmlspecialchars($d['uid']) ?>" <?= (($_GET['uid'] ?? '') === $d['uid']) ? 'selected' : '' ?>>
<?= htmlspecialchars($d['uid']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<select name="status" class="form-select">
<option value="">-- Todos os status --</option>
<?php foreach (['pending','sent','done','failed'] as $s): ?>
<option value="<?= $s ?>" <?= (($_GET['status'] ?? '') === $s) ? 'selected' : '' ?>>
<?= ucfirst($s) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<button class="btn btn-primary w-100">🔎 Filtrar</button>
</div>
</form>
<!-- Tabela -->
<div class="table-responsive">
<table class="table table-dark table-hover shadow-sm">
<thead>
<tr>
<th>ID</th>
<th>Dispositivo</th>
<th>Comando</th>
<th>Status</th>
<th>Enfileirado</th>
<th>Enviado</th>
<th>ACK</th>
<th>Erro</th>
<th>Resposta</th>
<th>Resp. At</th>
</tr>
</thead>
<tbody>
<?php foreach ($comandos as $c): ?>
<?php
switch ($c['status']) {
case 'pending': $badge = 'warning'; break;
case 'sent': $badge = 'info'; break;
case 'done': $badge = 'success'; break;
case 'failed': $badge = 'danger'; break;
default: $badge = 'secondary'; break;
}
?>
<tr>
<td><?= $c['id'] ?></td>
<td><?= htmlspecialchars($c['uid']) ?></td>
<td><?= htmlspecialchars($c['cmd']) ?></td>
<td><span class="badge bg-<?= $badge ?>"><?= $c['status'] ?></span></td>
<td><?= $c['queued_at'] ?></td>
<td><?= $c['sent_at'] ?></td>
<td><?= $c['ack_ts'] ?></td>
<td><?= htmlspecialchars($c['error'] ?? '') ?></td>
<td><?= htmlspecialchars($c['resposta'] ?? '') ?></td>
<td><?= $c['resposta_at'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="mt-4">
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
</div>
</div>
</body>
</html>
<?php
require_once __DIR__ . "/api/protecao.php";
try {
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Se pediu para limpar pendentes
if (isset($_POST['clear_pending'])) {
$pdo->exec("DELETE FROM comandos WHERE status = 'pending'");
$_SESSION['flash'] = "🧹 Todos os comandos pendentes foram removidos!";
header("Location: historico.php");
exit;
}
// Filtros
$where = [];
$params = [];
if (!empty($_GET['uid'])) {
$where[] = "c.uid = ?";
$params[] = $_GET['uid'];
}
if (!empty($_GET['status'])) {
$where[] = "c.status = ?";
$params[] = $_GET['status'];
}
$sql = "SELECT c.id, c.uid, c.cmd, c.status, c.queued_at, c.sent_at, c.ack_ts, c.error,
r.payload AS resposta, r.ts AS resposta_at
FROM comandos c
LEFT JOIN respostas r ON r.uid = c.uid
" . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
ORDER BY c.queued_at DESC
LIMIT 50";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$comandos = $stmt->fetchAll();
$devices = $pdo->query("SELECT DISTINCT uid FROM devices ORDER BY uid ASC")->fetchAll();
} catch (Exception $e) {
die("Erro: " . $e->getMessage());
}
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Histórico de Comandos</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #121212; color: #eee; }
.card, .table { background-color: #1e1e1e; color: #ddd; }
.table thead { background-color: #333; }
</style>
<script>
// Auto-refresh a cada 5 segundos
setTimeout(() => location.reload(), 5000);
</script>
</head>
<body class="p-4">
<div class="container-fluid">
<h2 class="mb-4 text-info">📜 Histórico de Comandos</h2>
<?php if (!empty($_SESSION['flash'])): ?>
<div class="alert alert-success"><?= $_SESSION['flash']; unset($_SESSION['flash']); ?></div>
<?php endif; ?>
<!-- Botão para limpar pendentes -->
<form method="POST" class="mb-3">
<button type="submit" name="clear_pending" class="btn btn-danger">
🧹 Limpar Pendentes
</button>
</form>
<!-- Filtros -->
<form method="GET" class="row g-3 mb-4">
<div class="col-md-4">
<select name="uid" class="form-select">
<option value="">-- Todos os dispositivos --</option>
<?php foreach ($devices as $d): ?>
<option value="<?= htmlspecialchars($d['uid']) ?>" <?= (($_GET['uid'] ?? '') === $d['uid']) ? 'selected' : '' ?>>
<?= htmlspecialchars($d['uid']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<select name="status" class="form-select">
<option value="">-- Todos os status --</option>
<?php foreach (['pending','sent','done','failed'] as $s): ?>
<option value="<?= $s ?>" <?= (($_GET['status'] ?? '') === $s) ? 'selected' : '' ?>>
<?= ucfirst($s) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<button class="btn btn-primary w-100">🔎 Filtrar</button>
</div>
</form>
<!-- Tabela -->
<div class="table-responsive">
<table class="table table-dark table-hover shadow-sm">
<thead>
<tr>
<th>ID</th>
<th>Dispositivo</th>
<th>Comando</th>
<th>Status</th>
<th>Enfileirado</th>
<th>Enviado</th>
<th>ACK</th>
<th>Erro</th>
<th>Resposta</th>
<th>Resp. At</th>
</tr>
</thead>
<tbody>
<?php foreach ($comandos as $c): ?>
<?php
switch ($c['status']) {
case 'pending': $badge = 'warning'; break;
case 'sent': $badge = 'info'; break;
case 'done': $badge = 'success'; break;
case 'failed': $badge = 'danger'; break;
default: $badge = 'secondary'; break;
}
?>
<tr>
<td><?= $c['id'] ?></td>
<td><?= htmlspecialchars($c['uid']) ?></td>
<td><?= htmlspecialchars($c['cmd']) ?></td>
<td><span class="badge bg-<?= $badge ?>"><?= $c['status'] ?></span></td>
<td><?= $c['queued_at'] ?></td>
<td><?= $c['sent_at'] ?></td>
<td><?= $c['ack_ts'] ?></td>
<td><?= htmlspecialchars($c['error'] ?? '') ?></td>
<td><?= htmlspecialchars($c['resposta'] ?? '') ?></td>
<td><?= $c['resposta_at'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="mt-4">
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
</div>
</div>
</body>
</html>

View File

@ -1,51 +1,51 @@
server_name: "matrix.xupas.mywire.org"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: synapse
password: synapse
database: synapse
host: matrix-postgres-1
cp_min: 5
cp_max: 10
allow_unsafe_locale: true
experimental_features:
msc3202_enabled: true
msc3077_enabled: true
msc3401_enabled: true
msc3391_enabled: true
msc3890_enabled: true
msc4153_enabled: true
turn_uris:
- "turn:192.168.10.253:5349?transport=udp"
- "turn:192.168.10.253:5349?transport=tcp"
turn_shared_secret: "c246729fea23718edc2bf08726f6822f6732b1bde3c2e062194fc5e6758b0cf7"
turn_user_lifetime: 86400000
turn_allow_guests: true
log_config: "/data/matrix.xupas.mywire.org.log.config"
media_store_path: /data/media_store
registration_shared_secret: "c#PmLj2F,GAR,xFWL.uw-Gb0TF47oKD^Nb@a~h~D@WRVBHdo*y"
report_stats: true
macaroon_secret_key: "5XLRSbZP&ij+Dh=SC4^hUL2SjleMkIS69=uPsS&n6D8aLqJGqA"
form_secret: "ht7o4U6Ue@S7HBDVVJczKH*4h_gBkADg@ursU0Ye*RBIn7R8w8"
signing_key_path: "/data/matrix.xupas.mywire.org.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
enable_registration: false
enable_registration_without_verification: false
server_name: "matrix.xupas.mywire.org"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: synapse
password: synapse
database: synapse
host: matrix-postgres-1
cp_min: 5
cp_max: 10
allow_unsafe_locale: true
experimental_features:
msc3202_enabled: true
msc3077_enabled: true
msc3401_enabled: true
msc3391_enabled: true
msc3890_enabled: true
msc4153_enabled: true
turn_uris:
- "turn:192.168.10.253:5349?transport=udp"
- "turn:192.168.10.253:5349?transport=tcp"
turn_shared_secret: "c246729fea23718edc2bf08726f6822f6732b1bde3c2e062194fc5e6758b0cf7"
turn_user_lifetime: 86400000
turn_allow_guests: true
log_config: "/data/matrix.xupas.mywire.org.log.config"
media_store_path: /data/media_store
registration_shared_secret: "c#PmLj2F,GAR,xFWL.uw-Gb0TF47oKD^Nb@a~h~D@WRVBHdo*y"
report_stats: true
macaroon_secret_key: "5XLRSbZP&ij+Dh=SC4^hUL2SjleMkIS69=uPsS&n6D8aLqJGqA"
form_secret: "ht7o4U6Ue@S7HBDVVJczKH*4h_gBkADg@ursU0Ye*RBIn7R8w8"
signing_key_path: "/data/matrix.xupas.mywire.org.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
enable_registration: false
enable_registration_without_verification: false

View File

@ -1,279 +1,279 @@
<?php declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
// ==============================
// ⚙️ CONFIGURAÇÃO
// ==============================
$MATRIX_BASE = "https://matrix.xupas.mywire.org";
$SERVER_NAME = "matrix.xupas.mywire.org";
$TOKEN_FILE = "/var/www/secure/matrix_token.txt";
$ADMIN_TOKEN = is_readable($TOKEN_FILE) ? trim(file_get_contents($TOKEN_FILE)) : '';
$DEBUG_LOG = "/tmp/matrix_debug.log";
// ==============================
// 📝 DEBUG HELPER
// ==============================
function dbg($txt) {
global $DEBUG_LOG;
file_put_contents($DEBUG_LOG, "[".date('Y-m-d H:i:s')."] $txt\n", FILE_APPEND);
}
// ==============================
// 🔧 API CALL
// ==============================
function call_api(string $method, string $url, ?array $data = null, ?string $token = null): array {
dbg("API CALL: $method $url DATA=" . json_encode($data));
$ch = curl_init($url);
$headers = ['Content-Type: application/json'];
if ($token) $headers[] = "Authorization: Bearer $token";
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 60,
]);
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
dbg("API RESP: HTTP=$code BODY=$resp");
curl_close($ch);
return [$code, $resp];
}
// ==============================
// 🧼 USERNAME CLEANER FIX
// ==============================
function full_user(string $name, string $server): string {
dbg("RAW POST USER=[".$name."] HEX=[".bin2hex($name)."]");
$clean = trim($name);
$clean = preg_replace('/[\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]/u', '', $clean);
dbg("CLEAN USER=[".$clean."] HEX=[".bin2hex($clean)."]");
if (preg_match('/^@[a-z0-9=_\-\.\/\+]+:[a-z0-9\.\-]+$/i', $clean)) return $clean;
if ($clean === '') return '';
if ($clean[0] !== '@') $clean = '@' . $clean;
if (!str_contains($clean, ':')) $clean .= ':' . $server;
dbg("FINAL USER=[".$clean."] HEX=[".bin2hex($clean)."]");
return $clean;
}
// ==============================
// 🚦 PROCESSAMENTO DE AÇÕES
// ==============================
$action = $_POST['action'] ?? null;
$msg = '';
if ($action && $ADMIN_TOKEN) {
dbg("=== ACTION: $action ===");
dbg("POST DATA: " . json_encode($_POST));
// criar utilizador
if ($action === 'add_user') {
$user = full_user($_POST['user'], $SERVER_NAME);
$pass = trim($_POST['pass']);
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
"password" => $pass,
"deactivated" => false,
"admin" => false
], $ADMIN_TOKEN);
$msg = ($code == 200 || $code == 201) ? "✅ Criado: $user" : "❌ Erro [$code]: $resp";
}
// alterar password
if ($action === 'change_pass') {
$user = full_user($_POST['user'], $SERVER_NAME);
$pass = trim($_POST['pass']);
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/reset_password/" . urlencode($user), [
"new_password" => $pass
], $ADMIN_TOKEN);
$msg = ($code == 200) ? "🔑 Senha alterada para $user" : "❌ Erro [$code]: $resp";
}
// admin / unadmin
if ($action === 'make_admin' || $action === 'revoke_admin') {
$user = full_user($_POST['user'], $SERVER_NAME);
$is_admin = $action === 'make_admin';
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
"admin" => $is_admin
], $ADMIN_TOKEN);
$msg = ($code == 200)
? ($is_admin ? "⬆️ $user agora é admin" : "⬇️ $user deixou de ser admin")
: "❌ Erro [$code]: $resp";
}
// ativar / desativar
if ($action === 'deactivate_user' || $action === 'reactivate_user') {
$user = full_user($_POST['user'], $SERVER_NAME);
$active = $action === 'reactivate_user';
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
"deactivated" => !$active
], $ADMIN_TOKEN);
$msg = ($code == 200)
? ($active ? "♻️ $user reativado" : "🚫 $user desativado")
: "❌ Erro [$code]: $resp";
}
// apagar user
if ($action === 'delete_user') {
$user = full_user($_POST['user'], $SERVER_NAME);
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/deactivate/" . urlencode($user), [
"erase" => true
], $ADMIN_TOKEN);
$msg = ($code == 200 || $code == 204)
? "🗑️ $user removido permanentemente"
: "❌ Erro [$code]: $resp";
}
// ✅ NOVA: apagar mensagens do user (mantendo o user)
if ($action === 'erase_messages') {
$user = full_user($_POST['user'], $SERVER_NAME);
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/user/$user/rooms", null, $ADMIN_TOKEN);
$rooms = json_decode($resp, true)['rooms'] ?? [];
$errors = [];
foreach ($rooms as $room) {
[$c, $r] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/purge_history/" . urlencode($room['room_id']), [
"delete_local_events" => true,
"reason" => "Apagadas via painel"
], $ADMIN_TOKEN);
if ($c != 200) $errors[] = "$room[room_id]: $r";
}
$msg = empty($errors) ? "🧹 Mensagens de $user apagadas" : "❌ Erros: " . implode("; ", $errors);
}
// ✅ NOVA: limpar metadados gerais
if ($action === 'clear_metadata') {
@unlink($DEBUG_LOG);
// aqui podes adicionar mais caminhos de logs/metadados
$msg = "⚡ Metadados limpos!";
}
}
// ==============================
// 📋 LISTAGEM USERS
// ==============================
$users = [];
if ($ADMIN_TOKEN) {
$next = null;
do {
$url = "$MATRIX_BASE/_synapse/admin/v2/users?limit=100";
if ($next) $url .= "&from=" . urlencode($next);
[$code, $resp] = call_api("GET", $url, null, $ADMIN_TOKEN);
if ($code != 200) { dbg("ERRO AO LER USERS: HTTP=$code BODY=$resp"); break; }
$data = json_decode($resp, true);
$chunk = $data['users'] ?? $data['results'] ?? [];
$users = array_merge($users, $chunk);
$next = $data['next_token'] ?? null;
} while ($next);
}
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="utf-8">
<title>Matrix Admin - Xupas</title>
<style>
body {background:#0e0e10;color:#eee;font-family:Segoe UI,Arial;margin:20px;}
.box{background:#1a1a1d;border:1px solid #333;border-radius:10px;padding:12px;margin-top:10px;}
h2{color:#3fa9f5;}
input,select,button{background:#202124;color:#eee;border:1px solid #333;border-radius:6px;padding:6px 8px;margin:2px;}
button:hover{background:#3fa9f5;color:#000;}
table{border-collapse:collapse;width:100%;margin-top:10px;}
th,td{border:1px solid #333;padding:6px;}
tr:nth-child(even){background:#171717;}
.msg{padding:8px;margin:8px 0;border-radius:6px;}
.ok{background:#12391f;color:#4caf50;}
.err{background:#3a1515;color:#ffb3b3;}
</style>
</head>
<body>
<h2>💻 Matrix Admin - Xupas</h2>
<?php if ($msg): ?>
<div class="msg <?=str_starts_with($msg,'❌')?'err':'ok'?>"><?=htmlspecialchars($msg)?></div>
<?php endif; ?>
<div class="box">
<h3> Criar Utilizador</h3>
<form method="post">
<input type="hidden" name="action" value="add_user">
<input type="text" name="user" placeholder="nome" required>
<input type="password" name="pass" placeholder="senha" required>
<button>Criar</button>
</form>
</div>
<div class="box">
<h3>👥 Utilizadores</h3>
<table>
<tr>
<th>User</th>
<th>Admin</th>
<th>Ativo</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
<?php foreach($users as $u):
$n = $u['name'] ?? $u['user_id'] ?? '—';
$seen = isset($u['last_seen_ts']) ? date('Y-m-d H:i:s', (int)($u['last_seen_ts'] / 1000)) : '—';
?>
<tr>
<td><?=$n?></td>
<td><?=!empty($u['admin'])?'🟢':'⚫'?></td>
<td><?=!empty($u['deactivated'])?'❌':'✅'?></td>
<td><?=$seen?></td>
<td>
<!-- Alterar senha -->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="change_pass">
<input type="hidden" name="user" value="<?=$n?>">
<input type="password" name="pass" placeholder="nova" style="width:90px">
<button>🔑</button>
</form>
<!-- Tornar admin / remover admin -->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="<?=!empty($u['admin'])?'revoke_admin':'make_admin'?>">
<input type="hidden" name="user" value="<?=$n?>">
<button><?=!empty($u['admin'])?'⬇️':'⬆️'?></button>
</form>
<!-- Ativar / desativar -->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="<?=!empty($u['deactivated'])?'reactivate_user':'deactivate_user'?>">
<input type="hidden" name="user" value="<?=$n?>">
<button><?=!empty($u['deactivated'])?'♻️':'🚫'?></button>
</form>
<!-- Apagar user -->
<form method="post" style="display:inline" onsubmit="return confirm('Apagar <?=$n?>?');">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user" value="<?=$n?>">
<button>🗑️</button>
</form>
<!-- NOVO: apagar mensagens do user -->
<form method="post" style="display:inline" onsubmit="return confirm('Apagar TODAS mensagens de <?=$n?>?');">
<input type="hidden" name="action" value="erase_messages">
<input type="hidden" name="user" value="<?=$n?>">
<button>🧹</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<!-- NOVO: limpar metadados gerais -->
<div class="box">
<h3> Limpar metadados gerais</h3>
<form method="post" onsubmit="return confirm('⚠️ Isto apagará logs e metadados, mas não afetará utilizadores ou mensagens. Continuar?');">
<input type="hidden" name="action" value="clear_metadata">
<button>🗑️ Limpar tudo</button>
</form>
</div>
</body>
</html>
<?php declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
// ==============================
// ⚙️ CONFIGURAÇÃO
// ==============================
$MATRIX_BASE = "https://matrix.xupas.mywire.org";
$SERVER_NAME = "matrix.xupas.mywire.org";
$TOKEN_FILE = "/var/www/secure/matrix_token.txt";
$ADMIN_TOKEN = is_readable($TOKEN_FILE) ? trim(file_get_contents($TOKEN_FILE)) : '';
$DEBUG_LOG = "/tmp/matrix_debug.log";
// ==============================
// 📝 DEBUG HELPER
// ==============================
function dbg($txt) {
global $DEBUG_LOG;
file_put_contents($DEBUG_LOG, "[".date('Y-m-d H:i:s')."] $txt\n", FILE_APPEND);
}
// ==============================
// 🔧 API CALL
// ==============================
function call_api(string $method, string $url, ?array $data = null, ?string $token = null): array {
dbg("API CALL: $method $url DATA=" . json_encode($data));
$ch = curl_init($url);
$headers = ['Content-Type: application/json'];
if ($token) $headers[] = "Authorization: Bearer $token";
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 60,
]);
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
dbg("API RESP: HTTP=$code BODY=$resp");
curl_close($ch);
return [$code, $resp];
}
// ==============================
// 🧼 USERNAME CLEANER FIX
// ==============================
function full_user(string $name, string $server): string {
dbg("RAW POST USER=[".$name."] HEX=[".bin2hex($name)."]");
$clean = trim($name);
$clean = preg_replace('/[\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]/u', '', $clean);
dbg("CLEAN USER=[".$clean."] HEX=[".bin2hex($clean)."]");
if (preg_match('/^@[a-z0-9=_\-\.\/\+]+:[a-z0-9\.\-]+$/i', $clean)) return $clean;
if ($clean === '') return '';
if ($clean[0] !== '@') $clean = '@' . $clean;
if (!str_contains($clean, ':')) $clean .= ':' . $server;
dbg("FINAL USER=[".$clean."] HEX=[".bin2hex($clean)."]");
return $clean;
}
// ==============================
// 🚦 PROCESSAMENTO DE AÇÕES
// ==============================
$action = $_POST['action'] ?? null;
$msg = '';
if ($action && $ADMIN_TOKEN) {
dbg("=== ACTION: $action ===");
dbg("POST DATA: " . json_encode($_POST));
// criar utilizador
if ($action === 'add_user') {
$user = full_user($_POST['user'], $SERVER_NAME);
$pass = trim($_POST['pass']);
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
"password" => $pass,
"deactivated" => false,
"admin" => false
], $ADMIN_TOKEN);
$msg = ($code == 200 || $code == 201) ? "✅ Criado: $user" : "❌ Erro [$code]: $resp";
}
// alterar password
if ($action === 'change_pass') {
$user = full_user($_POST['user'], $SERVER_NAME);
$pass = trim($_POST['pass']);
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/reset_password/" . urlencode($user), [
"new_password" => $pass
], $ADMIN_TOKEN);
$msg = ($code == 200) ? "🔑 Senha alterada para $user" : "❌ Erro [$code]: $resp";
}
// admin / unadmin
if ($action === 'make_admin' || $action === 'revoke_admin') {
$user = full_user($_POST['user'], $SERVER_NAME);
$is_admin = $action === 'make_admin';
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
"admin" => $is_admin
], $ADMIN_TOKEN);
$msg = ($code == 200)
? ($is_admin ? "⬆️ $user agora é admin" : "⬇️ $user deixou de ser admin")
: "❌ Erro [$code]: $resp";
}
// ativar / desativar
if ($action === 'deactivate_user' || $action === 'reactivate_user') {
$user = full_user($_POST['user'], $SERVER_NAME);
$active = $action === 'reactivate_user';
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
"deactivated" => !$active
], $ADMIN_TOKEN);
$msg = ($code == 200)
? ($active ? "♻️ $user reativado" : "🚫 $user desativado")
: "❌ Erro [$code]: $resp";
}
// apagar user
if ($action === 'delete_user') {
$user = full_user($_POST['user'], $SERVER_NAME);
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/deactivate/" . urlencode($user), [
"erase" => true
], $ADMIN_TOKEN);
$msg = ($code == 200 || $code == 204)
? "🗑️ $user removido permanentemente"
: "❌ Erro [$code]: $resp";
}
// ✅ NOVA: apagar mensagens do user (mantendo o user)
if ($action === 'erase_messages') {
$user = full_user($_POST['user'], $SERVER_NAME);
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/user/$user/rooms", null, $ADMIN_TOKEN);
$rooms = json_decode($resp, true)['rooms'] ?? [];
$errors = [];
foreach ($rooms as $room) {
[$c, $r] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/purge_history/" . urlencode($room['room_id']), [
"delete_local_events" => true,
"reason" => "Apagadas via painel"
], $ADMIN_TOKEN);
if ($c != 200) $errors[] = "$room[room_id]: $r";
}
$msg = empty($errors) ? "🧹 Mensagens de $user apagadas" : "❌ Erros: " . implode("; ", $errors);
}
// ✅ NOVA: limpar metadados gerais
if ($action === 'clear_metadata') {
@unlink($DEBUG_LOG);
// aqui podes adicionar mais caminhos de logs/metadados
$msg = "⚡ Metadados limpos!";
}
}
// ==============================
// 📋 LISTAGEM USERS
// ==============================
$users = [];
if ($ADMIN_TOKEN) {
$next = null;
do {
$url = "$MATRIX_BASE/_synapse/admin/v2/users?limit=100";
if ($next) $url .= "&from=" . urlencode($next);
[$code, $resp] = call_api("GET", $url, null, $ADMIN_TOKEN);
if ($code != 200) { dbg("ERRO AO LER USERS: HTTP=$code BODY=$resp"); break; }
$data = json_decode($resp, true);
$chunk = $data['users'] ?? $data['results'] ?? [];
$users = array_merge($users, $chunk);
$next = $data['next_token'] ?? null;
} while ($next);
}
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="utf-8">
<title>Matrix Admin - Xupas</title>
<style>
body {background:#0e0e10;color:#eee;font-family:Segoe UI,Arial;margin:20px;}
.box{background:#1a1a1d;border:1px solid #333;border-radius:10px;padding:12px;margin-top:10px;}
h2{color:#3fa9f5;}
input,select,button{background:#202124;color:#eee;border:1px solid #333;border-radius:6px;padding:6px 8px;margin:2px;}
button:hover{background:#3fa9f5;color:#000;}
table{border-collapse:collapse;width:100%;margin-top:10px;}
th,td{border:1px solid #333;padding:6px;}
tr:nth-child(even){background:#171717;}
.msg{padding:8px;margin:8px 0;border-radius:6px;}
.ok{background:#12391f;color:#4caf50;}
.err{background:#3a1515;color:#ffb3b3;}
</style>
</head>
<body>
<h2>💻 Matrix Admin - Xupas</h2>
<?php if ($msg): ?>
<div class="msg <?=str_starts_with($msg,'❌')?'err':'ok'?>"><?=htmlspecialchars($msg)?></div>
<?php endif; ?>
<div class="box">
<h3> Criar Utilizador</h3>
<form method="post">
<input type="hidden" name="action" value="add_user">
<input type="text" name="user" placeholder="nome" required>
<input type="password" name="pass" placeholder="senha" required>
<button>Criar</button>
</form>
</div>
<div class="box">
<h3>👥 Utilizadores</h3>
<table>
<tr>
<th>User</th>
<th>Admin</th>
<th>Ativo</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
<?php foreach($users as $u):
$n = $u['name'] ?? $u['user_id'] ?? '—';
$seen = isset($u['last_seen_ts']) ? date('Y-m-d H:i:s', (int)($u['last_seen_ts'] / 1000)) : '—';
?>
<tr>
<td><?=$n?></td>
<td><?=!empty($u['admin'])?'🟢':'⚫'?></td>
<td><?=!empty($u['deactivated'])?'❌':'✅'?></td>
<td><?=$seen?></td>
<td>
<!-- Alterar senha -->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="change_pass">
<input type="hidden" name="user" value="<?=$n?>">
<input type="password" name="pass" placeholder="nova" style="width:90px">
<button>🔑</button>
</form>
<!-- Tornar admin / remover admin -->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="<?=!empty($u['admin'])?'revoke_admin':'make_admin'?>">
<input type="hidden" name="user" value="<?=$n?>">
<button><?=!empty($u['admin'])?'⬇️':'⬆️'?></button>
</form>
<!-- Ativar / desativar -->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="<?=!empty($u['deactivated'])?'reactivate_user':'deactivate_user'?>">
<input type="hidden" name="user" value="<?=$n?>">
<button><?=!empty($u['deactivated'])?'♻️':'🚫'?></button>
</form>
<!-- Apagar user -->
<form method="post" style="display:inline" onsubmit="return confirm('Apagar <?=$n?>?');">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user" value="<?=$n?>">
<button>🗑️</button>
</form>
<!-- NOVO: apagar mensagens do user -->
<form method="post" style="display:inline" onsubmit="return confirm('Apagar TODAS mensagens de <?=$n?>?');">
<input type="hidden" name="action" value="erase_messages">
<input type="hidden" name="user" value="<?=$n?>">
<button>🧹</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<!-- NOVO: limpar metadados gerais -->
<div class="box">
<h3> Limpar metadados gerais</h3>
<form method="post" onsubmit="return confirm('⚠️ Isto apagará logs e metadados, mas não afetará utilizadores ou mensagens. Continuar?');">
<input type="hidden" name="action" value="clear_metadata">
<button>🗑️ Limpar tudo</button>
</form>
</div>
</body>
</html>

211
proxmox.php Normal file
View File

@ -0,0 +1,211 @@
<?php
require_once __DIR__.'/inc/common.php';
define('PMX_HOST', 'https://192.168.10.150:8006');
define('PMX_USER', 'root@pam');
define('PMX_PASS', 'master2000');
define('PMX_NODE', 'pve');
function pmx_auth(): ?array {
$ch = curl_init(PMX_HOST.'/api2/json/access/ticket');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['username' => PMX_USER, 'password' => PMX_PASS]),
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 6,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
return $r['data'] ?? null;
}
function pmx_get(string $path, array $auth): ?array {
$ch = curl_init(PMX_HOST.'/api2/json'.$path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 6,
CURLOPT_HTTPHEADER => ['Cookie: PVEAuthCookie='.$auth['ticket']],
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
return $r['data'] ?? null;
}
function fmt_bytes(int $b): string {
if ($b >= 1073741824) return round($b/1073741824, 1).' GB';
if ($b >= 1048576) return round($b/1048576, 1).' MB';
return round($b/1024, 1).' KB';
}
function fmt_uptime(int $s): string {
$d = intdiv($s, 86400); $h = intdiv($s % 86400, 3600); $m = intdiv($s % 3600, 60);
return $d ? "{$d}d {$h}h" : ($h ? "{$h}h {$m}m" : "{$m}m");
}
function pct_color(float $p): string {
if ($p >= 85) return '#f85149';
if ($p >= 60) return '#d29922';
return '#3fb950';
}
$auth = pmx_auth();
$error = null;
$node = $containers = $vms = null;
if (!$auth) {
$error = 'Não foi possível autenticar na API do Proxmox.';
} else {
$node = pmx_get('/nodes/'.PMX_NODE.'/status', $auth);
$storage = pmx_get('/nodes/'.PMX_NODE.'/storage', $auth) ?? [];
$containers = pmx_get('/nodes/'.PMX_NODE.'/lxc', $auth) ?? [];
$vms = pmx_get('/nodes/'.PMX_NODE.'/qemu', $auth) ?? [];
usort($containers, fn($a,$b) => $a['vmid'] <=> $b['vmid']);
usort($vms, fn($a,$b) => $a['vmid'] <=> $b['vmid']);
$storage = array_filter($storage, fn($s) => ($s['active'] ?? 0) && ($s['total'] ?? 0) > 0);
usort($storage, fn($a,$b) => strcmp($a['storage'], $b['storage']));
}
$title = 'Proxmox';
ob_start();
?>
<div class="page-header">
<div>
<div class="breadcrumb"><a href="/painel.php">Painel</a><span class="sep"></span>Proxmox</div>
<h1>Proxmox <span style="color:var(--muted);font-weight:400;font-size:16px"><?= PMX_NODE ?></span></h1>
</div>
<button onclick="location.reload()" class="btn btn-secondary"> Atualizar</button>
</div>
<?php if ($error): ?>
<div class="alert alert-error"> <?= h($error) ?></div>
<?php else: ?>
<?php if ($node):
$cpu_pct = round(($node['cpu'] ?? 0) * 100, 1);
$ram_used = $node['memory']['used'] ?? 0;
$ram_tot = $node['memory']['total'] ?? 1;
$ram_pct = round($ram_used / $ram_tot * 100, 1);
?>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:20px">
<?php foreach ([
['CPU', $cpu_pct.'%', $cpu_pct],
['RAM', fmt_bytes($ram_used).' / '.fmt_bytes($ram_tot), $ram_pct],
['Uptime', fmt_uptime($node['uptime'] ?? 0), -1],
] as [$label, $val, $pct]): ?>
<div class="card">
<div class="card-body" style="padding:16px">
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px"><?= $label ?></div>
<div style="font-size:18px;font-weight:600;color:<?= $pct >= 0 ? pct_color($pct) : 'var(--text)' ?>"><?= $val ?></div>
<?php if ($pct >= 0): ?>
<div style="margin-top:8px;height:4px;background:var(--surface2);border-radius:2px">
<div style="height:4px;border-radius:2px;background:<?= pct_color($pct) ?>;width:<?= min($pct,100) ?>%"></div>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php foreach ($storage as $s):
$s_used = $s['used'] ?? 0;
$s_tot = $s['total'] ?? 1;
$s_pct = round($s_used / $s_tot * 100, 1);
?>
<div class="card">
<div class="card-body" style="padding:16px">
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px"><?= h($s['storage']) ?></div>
<div style="font-size:18px;font-weight:600;color:<?= pct_color($s_pct) ?>"><?= fmt_bytes($s_used) ?></div>
<div style="font-size:11px;color:var(--muted);margin-top:2px">de <?= fmt_bytes($s_tot) ?></div>
<div style="margin-top:8px;height:4px;background:var(--surface2);border-radius:2px">
<div style="height:4px;border-radius:2px;background:<?= pct_color($s_pct) ?>;width:<?= min($s_pct,100) ?>%"></div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- LXC Containers -->
<?php if ($containers): ?>
<div class="card" style="margin-bottom:20px">
<div class="card-header">
<h2>Containers LXC <span style="color:var(--muted);font-weight:400;font-size:13px">(<?= count($containers) ?>)</span></h2>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>ID</th><th>Nome</th><th>Estado</th><th>CPU</th><th>RAM</th><th>Uptime</th></tr></thead>
<tbody>
<?php foreach ($containers as $c):
$running = ($c['status'] === 'running');
$cpu_p = $running ? round(($c['cpu'] ?? 0) * 100, 1) : 0;
$ram_p = $running && ($c['maxmem'] ?? 0) ? round(($c['mem'] ?? 0) / $c['maxmem'] * 100, 1) : 0;
?>
<tr>
<td style="font-family:monospace;font-size:12px;color:var(--muted)"><?= $c['vmid'] ?></td>
<td style="font-weight:500"><?= h($c['name']) ?></td>
<td><?php if($running):?><span class="badge" style="background:#0d2818;color:#3fb950">running</span><?php else:?><span class="badge" style="background:#1e2733;color:var(--muted)"><?= h($c['status']) ?></span><?php endif;?></td>
<td style="font-size:12px">
<?php if($running):?>
<span style="color:<?= pct_color($cpu_p) ?>"><?= $cpu_p ?>%</span>
<?php else: ?><span style="color:var(--muted)">—</span><?php endif;?>
</td>
<td style="font-size:12px">
<?php if($running && ($c['maxmem']??0)):?>
<span style="color:<?= pct_color($ram_p) ?>"><?= fmt_bytes($c['mem']??0) ?></span>
<span style="color:var(--muted)"> / <?= fmt_bytes($c['maxmem']) ?></span>
<?php else: ?><span style="color:var(--muted)">—</span><?php endif;?>
</td>
<td style="color:var(--muted);font-size:12px"><?= $running ? fmt_uptime($c['uptime']??0) : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- VMs -->
<?php if ($vms): ?>
<div class="card">
<div class="card-header">
<h2>VMs <span style="color:var(--muted);font-weight:400;font-size:13px">(<?= count($vms) ?>)</span></h2>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>ID</th><th>Nome</th><th>Estado</th><th>CPU</th><th>RAM</th><th>Uptime</th></tr></thead>
<tbody>
<?php foreach ($vms as $v):
$running = ($v['status'] === 'running');
$cpu_p = $running ? round(($v['cpu'] ?? 0) * 100, 1) : 0;
$ram_p = $running && ($v['maxmem'] ?? 0) ? round(($v['mem'] ?? 0) / $v['maxmem'] * 100, 1) : 0;
?>
<tr>
<td style="font-family:monospace;font-size:12px;color:var(--muted)"><?= $v['vmid'] ?></td>
<td style="font-weight:500"><?= h($v['name']) ?></td>
<td><?php if($running):?><span class="badge" style="background:#0d2818;color:#3fb950">running</span><?php else:?><span class="badge" style="background:#1e2733;color:var(--muted)"><?= h($v['status']) ?></span><?php endif;?></td>
<td style="font-size:12px">
<?php if($running):?><span style="color:<?= pct_color($cpu_p) ?>"><?= $cpu_p ?>%</span>
<?php else: ?><span style="color:var(--muted)">—</span><?php endif;?>
</td>
<td style="font-size:12px">
<?php if($running && ($v['maxmem']??0)):?>
<span style="color:<?= pct_color($ram_p) ?>"><?= fmt_bytes($v['mem']??0) ?></span>
<span style="color:var(--muted)"> / <?= fmt_bytes($v['maxmem']) ?></span>
<?php else: ?><span style="color:var(--muted)">—</span><?php endif;?>
</td>
<td style="color:var(--muted);font-size:12px"><?= $running ? fmt_uptime($v['uptime']??0) : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';

View File

@ -1,31 +1,31 @@
<?php
require __DIR__.'/vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
$config = require __DIR__.'/config.php';
$topic = $_POST['topic'] ?? $_GET['topic'] ?? null;
$msg = $_POST['msg'] ?? $_GET['msg'] ?? null;
$qos = (int)($_POST['qos'] ?? 0);
$retain = (bool)($_POST['retain'] ?? false);
if (!$topic || $msg === null) {
http_response_code(400);
die('missing topic or msg');
}
$settings = (new ConnectionSettings)
->setUsername($config['username'])
->setPassword($config['password'])
->setKeepAliveInterval($config['keepalive'])
->setConnectTimeout($config['timeout'])
->setUseTls(false); // muda para true quando fores usar 8883/TLS
$client = new MqttClient($config['host'], $config['port'], $config['clientId']);
$client->connect($settings, $config['clean']);
$client->publish($topic, $msg, $qos, $retain);
$client->disconnect();
header('Content-Type: application/json');
echo json_encode(['ok'=>true,'topic'=>$topic,'qos'=>$qos,'retain'=>$retain]);
<?php
require __DIR__.'/vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
$config = require __DIR__.'/config.php';
$topic = $_POST['topic'] ?? $_GET['topic'] ?? null;
$msg = $_POST['msg'] ?? $_GET['msg'] ?? null;
$qos = (int)($_POST['qos'] ?? 0);
$retain = (bool)($_POST['retain'] ?? false);
if (!$topic || $msg === null) {
http_response_code(400);
die('missing topic or msg');
}
$settings = (new ConnectionSettings)
->setUsername($config['username'])
->setPassword($config['password'])
->setKeepAliveInterval($config['keepalive'])
->setConnectTimeout($config['timeout'])
->setUseTls(false); // muda para true quando fores usar 8883/TLS
$client = new MqttClient($config['host'], $config['port'], $config['clientId']);
$client->connect($settings, $config['clean']);
$client->publish($topic, $msg, $qos, $retain);
$client->disconnect();
header('Content-Type: application/json');
echo json_encode(['ok'=>true,'topic'=>$topic,'qos'=>$qos,'retain'=>$retain]);

View File

@ -1,16 +1,16 @@
<?php
require_once __DIR__ . "/api/protecao.php";
require __DIR__ . "/api/db.php";
$uid = $_POST['uid'] ?? '';
$note = $_POST['note'] ?? '';
$stmt = $pdo->prepare("
UPDATE contadores
SET note = ?
WHERE uid = ?
");
$stmt->execute([$note, $uid]);
<?php
require_once __DIR__ . "/api/protecao.php";
require __DIR__ . "/api/db.php";
$uid = $_POST['uid'] ?? '';
$note = $_POST['note'] ?? '';
$stmt = $pdo->prepare("
UPDATE contadores
SET note = ?
WHERE uid = ?
");
$stmt->execute([$note, $uid]);
header("Location: console.php");

View File

@ -1,25 +1,25 @@
<?php
// ---------------- DB ----------------
$dbHost = "localhost";
$dbName = "cagalhao";
$dbUser = "master";
$dbPass = "master";
try {
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Erro DB: " . $e->getMessage());
}
// ---------------- INPUT ----------------
$uid = $_GET['uid'] ?? '';
if ($uid) {
$stmt = $db->prepare("INSERT INTO comandos (uid, cmd, params, status)
VALUES (:uid, 'PING', '{}', 'pending')");
$stmt->execute([':uid' => $uid]);
// echo "PING enviado para {$uid}";
} else {
echo "Falta o UID.";
}
<?php
// ---------------- DB ----------------
$dbHost = "localhost";
$dbName = "cagalhao";
$dbUser = "master";
$dbPass = "master";
try {
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Erro DB: " . $e->getMessage());
}
// ---------------- INPUT ----------------
$uid = $_GET['uid'] ?? '';
if ($uid) {
$stmt = $db->prepare("INSERT INTO comandos (uid, cmd, params, status)
VALUES (:uid, 'PING', '{}', 'pending')");
$stmt->execute([':uid' => $uid]);
// echo "PING enviado para {$uid}";
} else {
echo "Falta o UID.";
}

View File

@ -1,25 +1,25 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"

228
vpn.php Normal file
View File

@ -0,0 +1,228 @@
<?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';