commit-all-changes
This commit is contained in:
parent
0d8cf8a7e5
commit
bea0c5c200
@ -1,35 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require __DIR__ . "/db.php";
|
require __DIR__ . "/db.php";
|
||||||
|
|
||||||
$rows = $pdo->query("
|
$rows = $pdo->query("
|
||||||
SELECT uid, note, entradas, saidas, online, blocked,
|
SELECT uid, note, entradas, saidas, online, blocked,
|
||||||
current_broker, rssi, heap, uptime,
|
current_broker, rssi, heap, uptime,
|
||||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||||
FROM contadores
|
FROM contadores
|
||||||
ORDER BY online DESC, uid ASC
|
ORDER BY online DESC, uid ASC
|
||||||
")->fetchAll();
|
")->fetchAll();
|
||||||
|
|
||||||
// Totais
|
// Totais
|
||||||
$sumEntradas = 0;
|
$sumEntradas = 0;
|
||||||
$sumSaidas = 0;
|
$sumSaidas = 0;
|
||||||
$onlineCount = 0;
|
$onlineCount = 0;
|
||||||
$offlineCount = 0;
|
$offlineCount = 0;
|
||||||
|
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$sumEntradas += (int)$r['entradas'];
|
$sumEntradas += (int)$r['entradas'];
|
||||||
$sumSaidas += (int)$r['saidas'];
|
$sumSaidas += (int)$r['saidas'];
|
||||||
if ($r['online'] == 1) $onlineCount++;
|
if ($r['online'] == 1) $onlineCount++;
|
||||||
else $offlineCount++;
|
else $offlineCount++;
|
||||||
}
|
}
|
||||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||||
header("Pragma: no-cache");
|
header("Pragma: no-cache");
|
||||||
header("Expires: 0");
|
header("Expires: 0");
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"rows" => $rows,
|
"rows" => $rows,
|
||||||
"online" => $onlineCount,
|
"online" => $onlineCount,
|
||||||
"offline" => $offlineCount,
|
"offline" => $offlineCount,
|
||||||
"entradas" => $sumEntradas,
|
"entradas" => $sumEntradas,
|
||||||
"saidas" => $sumSaidas,
|
"saidas" => $sumSaidas,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -50,29 +50,12 @@ $db = new PDO(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// =====================================================
|
// ─── migração: coluna auth_nonce_at (idempotente) ──────
|
||||||
// RECOVERY MQTT
|
$db->exec("
|
||||||
// =====================================================
|
ALTER TABLE contadores
|
||||||
|
ADD COLUMN IF NOT EXISTS
|
||||||
$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
|
auth_nonce_at DATETIME NULL DEFAULT NULL
|
||||||
$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';
|
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// GLOBAL SECRET
|
// GLOBAL SECRET
|
||||||
@ -81,28 +64,92 @@ $certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
|
|||||||
$authSecret = 'K82A91F0';
|
$authSecret = 'K82A91F0';
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// MQTT SETTINGS
|
// CA CERT mqtt-recovery.xpto.ooguy.com (self-signed)
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
$settings = (new ConnectionSettings())
|
$recoveryXptoCaCert = <<<'EOT'
|
||||||
->setUsername($recoveryUser)
|
-----BEGIN CERTIFICATE-----
|
||||||
->setPassword($recoveryPass)
|
MIIDuTCCAqGgAwIBAgIUass5a34ekbqg0iaygCtiGRiSFHIwDQYJKoZIhvcNAQEL
|
||||||
->setUseTls(true)
|
BQAwbDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
->setTlsSelfSignedAllowed(true)
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDElMCMGA1UEAwwcbXF0dC1yZWNvdmVy
|
||||||
->setKeepAliveInterval(30)
|
eS54cHRvLm9vZ3V5LmNvbTAeFw0yNjA1MjYxMjM1NDFaFw0zNjA1MjMxMjM1NDFa
|
||||||
->setConnectTimeout(5)
|
MGwxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ
|
||||||
->setSocketTimeout(1)
|
bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxJTAjBgNVBAMMHG1xdHQtcmVjb3Zlcnku
|
||||||
->setReconnectAutomatically(true);
|
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(
|
$brokerPairs = [
|
||||||
$recoveryHost,
|
|
||||||
$recoveryPort,
|
// ── Remoto — Let's Encrypt ─────────────────────
|
||||||
'listener-recovery-' . getmypid()
|
[
|
||||||
);
|
'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
|
// MAIN BROKER CHECK
|
||||||
@ -206,18 +253,41 @@ function send_cert_chunks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// SUBSCRIPTIONS
|
// MAKE SETTINGS
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
$subscribe = function () use (
|
function make_settings(array $pair): ConnectionSettings
|
||||||
$mqtt,
|
{
|
||||||
$db,
|
$s = (new ConnectionSettings())
|
||||||
$certPath,
|
->setUsername($pair['recvUser'])
|
||||||
$certUrl,
|
->setPassword($pair['recvPass'])
|
||||||
$mainHost,
|
->setUseTls(true)
|
||||||
$mainPort,
|
->setTlsSelfSignedAllowed($pair['selfSigned'])
|
||||||
$authSecret
|
->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
|
// AUTH
|
||||||
@ -226,7 +296,7 @@ $subscribe = function () use (
|
|||||||
$mqtt->subscribe(
|
$mqtt->subscribe(
|
||||||
'esp/+/auth',
|
'esp/+/auth',
|
||||||
function (string $topic, string $msg)
|
function (string $topic, string $msg)
|
||||||
use ($db, $mqtt, $authSecret)
|
use ($db, $mqtt, $authSecret, $label)
|
||||||
{
|
{
|
||||||
|
|
||||||
$uid = explode('/', $topic)[1] ?? null;
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
@ -235,17 +305,12 @@ $subscribe = function () use (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logmsg("AUTH RX $uid => $msg");
|
logmsg("[$label] AUTH RX $uid => $msg");
|
||||||
|
|
||||||
$json = json_decode($msg, true);
|
$json = json_decode($msg, true);
|
||||||
|
|
||||||
if (!$json) {
|
if (!$json) {
|
||||||
|
logmsg("[$label] JSON AUTH inválido", LOG_ERROR);
|
||||||
logmsg(
|
|
||||||
"JSON AUTH inválido",
|
|
||||||
LOG_ERROR
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,41 +320,62 @@ $subscribe = function () use (
|
|||||||
|
|
||||||
if (($json['cmd'] ?? '') === 'HELLO') {
|
if (($json['cmd'] ?? '') === 'HELLO') {
|
||||||
|
|
||||||
$nonce =
|
// ─── Idempotência: evitar AUTH_CHALLENGE duplicado ─
|
||||||
strtoupper(
|
// Quando o listener reinicia, o broker pode reentrogar
|
||||||
bin2hex(random_bytes(16))
|
// 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("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO contadores
|
SELECT auth_nonce, auth_nonce_at
|
||||||
(uid, auth_nonce, last_seen, online, blocked)
|
FROM contadores
|
||||||
VALUES
|
WHERE uid=:uid
|
||||||
(:uid, :nonce, NOW(), 1, 1)
|
LIMIT 1
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
auth_nonce=:nonce,
|
|
||||||
last_seen=NOW(),
|
|
||||||
online=1
|
|
||||||
");
|
");
|
||||||
/* novos ESPs entram com blocked=1 (pendente aprovação)
|
$stmt->execute(['uid' => $uid]);
|
||||||
ESPs existentes mantêm o blocked actual */
|
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$stmt->execute([
|
$existingNonce = $existing['auth_nonce'] ?? null;
|
||||||
'uid' => $uid,
|
$nonceAge = $existing['auth_nonce_at']
|
||||||
'nonce' => $nonce
|
? (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(
|
$mqtt->publish(
|
||||||
"esp/$uid/cmd",
|
"esp/$uid/cmd",
|
||||||
json_encode([
|
json_encode([
|
||||||
'cmd' => 'AUTH_CHALLENGE',
|
'cmd' => 'AUTH_CHALLENGE',
|
||||||
'nonce' => $nonce
|
'nonce' => $nonce,
|
||||||
]),
|
]),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
logmsg(
|
logmsg("[$label] AUTH_CHALLENGE enviado para $uid");
|
||||||
"AUTH_CHALLENGE enviado para $uid"
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -306,12 +392,7 @@ $subscribe = function () use (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!$hash) {
|
if (!$hash) {
|
||||||
|
logmsg("[$label] AUTH sem hash", LOG_ERROR);
|
||||||
logmsg(
|
|
||||||
"AUTH sem hash",
|
|
||||||
LOG_ERROR
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,29 +407,17 @@ $subscribe = function () use (
|
|||||||
'uid' => $uid
|
'uid' => $uid
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$row =
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$row) {
|
if (!$row) {
|
||||||
|
logmsg("[$label] UID desconhecido: $uid", LOG_ERROR);
|
||||||
logmsg(
|
|
||||||
"UID desconhecido: $uid",
|
|
||||||
LOG_ERROR
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$nonce =
|
$nonce = $row['auth_nonce'] ?? '';
|
||||||
$row['auth_nonce'] ?? '';
|
|
||||||
|
|
||||||
if (!$nonce) {
|
if (!$nonce) {
|
||||||
|
logmsg("[$label] Nonce vazio para $uid", LOG_ERROR);
|
||||||
logmsg(
|
|
||||||
"Nonce vazio para $uid",
|
|
||||||
LOG_ERROR
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +435,10 @@ $subscribe = function () use (
|
|||||||
// verificar se está aprovado
|
// verificar se está aprovado
|
||||||
if ((int)($row['blocked'] ?? 1) === 1) {
|
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(
|
$mqtt->publish(
|
||||||
"esp/$uid/cmd",
|
"esp/$uid/cmd",
|
||||||
@ -377,12 +449,13 @@ $subscribe = function () use (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logmsg("AUTH OK $uid");
|
logmsg("[$label] AUTH OK $uid");
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
UPDATE contadores
|
UPDATE contadores
|
||||||
SET
|
SET
|
||||||
auth_nonce=NULL,
|
auth_nonce=NULL,
|
||||||
|
auth_nonce_at=NULL,
|
||||||
auth_ok=1,
|
auth_ok=1,
|
||||||
online=1,
|
online=1,
|
||||||
last_seen=NOW()
|
last_seen=NOW()
|
||||||
@ -403,18 +476,10 @@ $subscribe = function () use (
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
logmsg(
|
// Hash nao bate: quase sempre e cross-talk - o device respondeu
|
||||||
"AUTH FAIL $uid",
|
// ao nonce do OUTRO listener (VPS). NAO mandar AUTH_FAIL (causava
|
||||||
LOG_ERROR
|
// boot-loop). So o listener cujo nonce bate manda AUTH_OK.
|
||||||
);
|
logmsg("[$label] AUTH hash mismatch $uid - ignorado (cross-talk)", LOG_ERROR);
|
||||||
|
|
||||||
$mqtt->publish(
|
|
||||||
"esp/$uid/cmd",
|
|
||||||
json_encode([
|
|
||||||
'cmd' => 'AUTH_FAIL'
|
|
||||||
]),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -433,9 +498,7 @@ $subscribe = function () use (
|
|||||||
string $msg
|
string $msg
|
||||||
) use (
|
) use (
|
||||||
$db,
|
$db,
|
||||||
$mqtt,
|
$label
|
||||||
$mainHost,
|
|
||||||
$mainPort
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
$uid = explode('/', $topic)[1] ?? null;
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
@ -474,41 +537,7 @@ $subscribe = function () use (
|
|||||||
'uid' => $uid
|
'uid' => $uid
|
||||||
]);
|
]);
|
||||||
|
|
||||||
logmsg(
|
logmsg("[$label] RECOVERY STATUS $uid => $msg");
|
||||||
"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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
0
|
0
|
||||||
@ -523,18 +552,15 @@ $subscribe = function () use (
|
|||||||
function (
|
function (
|
||||||
string $topic,
|
string $topic,
|
||||||
string $msg
|
string $msg
|
||||||
) {
|
) use ($label) {
|
||||||
|
|
||||||
$uid =
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
explode('/', $topic)[1] ?? null;
|
|
||||||
|
|
||||||
if (!$uid) {
|
if (!$uid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logmsg(
|
logmsg("[$label] RECOVERY EVENT $uid => $msg");
|
||||||
"RECOVERY EVENT $uid => $msg"
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
},
|
||||||
0
|
0
|
||||||
@ -552,100 +578,165 @@ $subscribe = function () use (
|
|||||||
) use (
|
) use (
|
||||||
$mqtt,
|
$mqtt,
|
||||||
$certPath,
|
$certPath,
|
||||||
$certUrl
|
$label
|
||||||
) {
|
) {
|
||||||
|
|
||||||
$uid =
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
explode('/', $topic)[1] ?? null;
|
|
||||||
|
|
||||||
if (!$uid) {
|
if (!$uid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logmsg("CERT_REQUEST de $uid → chunks");
|
logmsg("[$label] CERT_REQUEST de $uid → chunks");
|
||||||
|
|
||||||
send_cert_chunks($mqtt, $uid, $certPath);
|
send_cert_chunks($mqtt, $uid, $certPath);
|
||||||
|
|
||||||
},
|
},
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// MAIN LOOP
|
// 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) {
|
while (true) {
|
||||||
|
|
||||||
try {
|
// ── connect / reconnect ────────────────────────
|
||||||
|
|
||||||
logmsg(
|
foreach ($brokerPairs as $pair) {
|
||||||
"Ligar recovery $recoveryHost:$recoveryPort"
|
|
||||||
);
|
|
||||||
|
|
||||||
$mqtt->connect($settings, false);
|
$label = $pair['label'];
|
||||||
|
|
||||||
$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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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(
|
try {
|
||||||
"ERRO recovery: " .
|
|
||||||
$e->getMessage(),
|
|
||||||
LOG_ERROR
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
164
api/menu.php
164
api/menu.php
@ -1,163 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once "protecao.php";
|
header("Location: /vpn.php");
|
||||||
|
exit;
|
||||||
/* =========================
|
|
||||||
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>
|
|
||||||
|
|
||||||
|
|||||||
22
api/mqtt.php
22
api/mqtt.php
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
function mqtt_publish(string $topic, string $msg): void {
|
function mqtt_publish(string $topic, string $msg): void {
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
|
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
|
||||||
escapeshellarg($topic),
|
escapeshellarg($topic),
|
||||||
escapeshellarg($msg)
|
escapeshellarg($msg)
|
||||||
);
|
);
|
||||||
shell_exec($cmd . " > /dev/null 2>&1");
|
shell_exec($cmd . " > /dev/null 2>&1");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +1,44 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset de parciais com acumulação em totais (por máquina):
|
* Reset de parciais com acumulação em totais (por máquina):
|
||||||
* total += parcial; parcial = 0
|
* total += parcial; parcial = 0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED & ~E_NOTICE & ~E_USER_NOTICE);
|
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('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
header('Pragma: no-cache');
|
header('Pragma: no-cache');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
|
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// garantir colunas (ignora se já existirem)
|
// 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 entradas_total INT NOT NULL DEFAULT 0");
|
||||||
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_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'] ?? '';
|
$uid = $_POST['uid'] ?? '';
|
||||||
if ($uid === '') {
|
if ($uid === '') {
|
||||||
header("Location: /console.php?ok=err");
|
header("Location: /console.php?ok=err");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
UPDATE contadores
|
UPDATE contadores
|
||||||
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
|
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
|
||||||
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
|
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
|
||||||
entradas = 0,
|
entradas = 0,
|
||||||
saidas = 0
|
saidas = 0
|
||||||
WHERE uid = :uid
|
WHERE uid = :uid
|
||||||
");
|
");
|
||||||
$stmt->execute(['uid' => $uid]);
|
$stmt->execute(['uid' => $uid]);
|
||||||
|
|
||||||
header("Location: /console.php?ok=line");
|
header("Location: /console.php?ok=line");
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
header("Location: /console.php?ok=err");
|
header("Location: /console.php?ok=err");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
|
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
|
||||||
|
|
||||||
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
|
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
|
||||||
$stmt->execute([$newPassword]);
|
$stmt->execute([$newPassword]);
|
||||||
|
|
||||||
echo "✅ Password de admin alterada para admin123";
|
echo "✅ Password de admin alterada para admin123";
|
||||||
|
|||||||
@ -1,34 +1,34 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
require __DIR__ . '/db.php'; // ligação PDO
|
require __DIR__ . '/db.php'; // ligação PDO
|
||||||
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
|
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
|
||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
|
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error'=>'invalid_request']);
|
echo json_encode(['error'=>'invalid_request']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uid = $data['uid'];
|
$uid = $data['uid'];
|
||||||
$cmd = $data['cmd'];
|
$cmd = $data['cmd'];
|
||||||
$value = $data['value'] ?? null;
|
$value = $data['value'] ?? null;
|
||||||
|
|
||||||
// ----------------------- MQTT -----------------------
|
// ----------------------- MQTT -----------------------
|
||||||
$topic = "esp/$uid/cmd";
|
$topic = "esp/$uid/cmd";
|
||||||
|
|
||||||
$msg = ['cmd' => $cmd];
|
$msg = ['cmd' => $cmd];
|
||||||
if ($value !== null) $msg['value'] = $value;
|
if ($value !== null) $msg['value'] = $value;
|
||||||
|
|
||||||
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
|
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
// ----------------------- BD -------------------------
|
// ----------------------- BD -------------------------
|
||||||
if ($cmd === 'BLOCK') {
|
if ($cmd === 'BLOCK') {
|
||||||
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
|
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
|
||||||
$stmt->execute([intval($value), $uid]);
|
$stmt->execute([intval($value), $uid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['ok'=>1]);
|
echo json_encode(['ok'=>1]);
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
set_time_limit(30);
|
set_time_limit(30);
|
||||||
require __DIR__ . '/vendor/autoload.php';
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
use PhpMqtt\Client\MqttClient;
|
use PhpMqtt\Client\MqttClient;
|
||||||
use PhpMqtt\Client\ConnectionSettings;
|
use PhpMqtt\Client\ConnectionSettings;
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
|
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
|
||||||
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
|
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
|
||||||
$mqtt->connect($settings, true);
|
$mqtt->connect($settings, true);
|
||||||
|
|
||||||
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
|
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
|
||||||
$data[] = ['topic'=>$topic, 'message'=>$message];
|
$data[] = ['topic'=>$topic, 'message'=>$message];
|
||||||
if (count($data) >= 5) {
|
if (count($data) >= 5) {
|
||||||
$mqtt->interrupt(); // para o loop depois de 5 msg
|
$mqtt->interrupt(); // para o loop depois de 5 msg
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
$mqtt->loop(true);
|
$mqtt->loop(true);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode($data, JSON_PRETTY_PRINT);
|
echo json_encode($data, JSON_PRETTY_PRINT);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ try {
|
|||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
|
|
||||||
// Redireciona para o menu principal
|
// Redireciona para o menu principal
|
||||||
header("Location: /api/menu.php");
|
header("Location: /painel.php");
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
50
api/vendor/php-mqtt/client/.github/release.yml
vendored
50
api/vendor/php-mqtt/client/.github/release.yml
vendored
@ -1,25 +1,25 @@
|
|||||||
changelog:
|
changelog:
|
||||||
exclude:
|
exclude:
|
||||||
labels:
|
labels:
|
||||||
- ignore-for-release
|
- ignore-for-release
|
||||||
authors:
|
authors:
|
||||||
- octocat
|
- octocat
|
||||||
categories:
|
categories:
|
||||||
- title: Added
|
- title: Added
|
||||||
labels:
|
labels:
|
||||||
- enhancement
|
- enhancement
|
||||||
- title: Deprecated
|
- title: Deprecated
|
||||||
labels:
|
labels:
|
||||||
- deprecated
|
- deprecated
|
||||||
- title: Removed
|
- title: Removed
|
||||||
labels:
|
labels:
|
||||||
- removed
|
- removed
|
||||||
- title: Fixed
|
- title: Fixed
|
||||||
labels:
|
labels:
|
||||||
- bug
|
- bug
|
||||||
- title: Security
|
- title: Security
|
||||||
labels:
|
labels:
|
||||||
- security
|
- security
|
||||||
- title: Changed
|
- title: Changed
|
||||||
labels:
|
labels:
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
110
comandosbck.php
110
comandosbck.php
@ -1,55 +1,55 @@
|
|||||||
<?php
|
<?php
|
||||||
// ---------------- DB ----------------
|
// ---------------- DB ----------------
|
||||||
$dbHost = "localhost";
|
$dbHost = "localhost";
|
||||||
$dbName = "cagalhao";
|
$dbName = "cagalhao";
|
||||||
$dbUser = "master";
|
$dbUser = "master";
|
||||||
$dbPass = "master";
|
$dbPass = "master";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
||||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erro DB: " . $e->getMessage());
|
die("Erro DB: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
|
$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
|
||||||
$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Histórico de Comandos</title>
|
<title>Histórico de Comandos</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; background: #111; color: #eee; }
|
body { font-family: Arial, sans-serif; background: #111; color: #eee; }
|
||||||
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
|
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
|
||||||
th, td { border: 1px solid #444; padding: 6px 10px; text-align: center; }
|
th, td { border: 1px solid #444; padding: 6px 10px; text-align: center; }
|
||||||
th { background: #222; }
|
th { background: #222; }
|
||||||
tr:nth-child(even) { background: #1b1b1b; }
|
tr:nth-child(even) { background: #1b1b1b; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Histórico de Comandos</h1>
|
<h1>Histórico de Comandos</h1>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>UID</th>
|
<th>UID</th>
|
||||||
<th>CMD</th>
|
<th>CMD</th>
|
||||||
<th>Params</th>
|
<th>Params</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Data</th>
|
<th>Data</th>
|
||||||
</tr>
|
</tr>
|
||||||
<?php foreach ($comandos as $cmd): ?>
|
<?php foreach ($comandos as $cmd): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= $cmd['id'] ?></td>
|
<td><?= $cmd['id'] ?></td>
|
||||||
<td><?= htmlspecialchars($cmd['uid']) ?></td>
|
<td><?= htmlspecialchars($cmd['uid']) ?></td>
|
||||||
<td><?= htmlspecialchars($cmd['cmd']) ?></td>
|
<td><?= htmlspecialchars($cmd['cmd']) ?></td>
|
||||||
<td><pre><?= htmlspecialchars($cmd['params']) ?></pre></td>
|
<td><pre><?= htmlspecialchars($cmd['params']) ?></pre></td>
|
||||||
<td><?= $cmd['status'] ?></td>
|
<td><?= $cmd['status'] ?></td>
|
||||||
<td><?= $cmd['created_at'] ?? '' ?></td>
|
<td><?= $cmd['created_at'] ?? '' ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</table>
|
</table>
|
||||||
<p><a href="/api/menu.php">⬅️ Voltar</a></p>
|
<p><a href="/api/menu.php">⬅️ Voltar</a></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
22
config.php
22
config.php
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'host' => 'mqtt.xupas.mywire.org',
|
'host' => 'mqtt.xupas.mywire.org',
|
||||||
'port' => 8883,
|
'port' => 8883,
|
||||||
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
|
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
|
||||||
'username' => 'xupa',
|
'username' => 'xupa',
|
||||||
'password' => 'xupa',
|
'password' => 'xupa',
|
||||||
'clean' => true,
|
'clean' => true,
|
||||||
'keepalive'=> 60,
|
'keepalive'=> 60,
|
||||||
'timeout' => 5,
|
'timeout' => 5,
|
||||||
];
|
];
|
||||||
|
|||||||
1320
console.php
1320
console.php
File diff suppressed because it is too large
Load Diff
32
devices.php
32
devices.php
@ -122,6 +122,7 @@ $pending = $pdo->query("
|
|||||||
|
|
||||||
$approved = $pdo->query("
|
$approved = $pdo->query("
|
||||||
SELECT uid, note, last_seen, online, blocked, auth_ok, current_broker,
|
SELECT uid, note, last_seen, online, blocked, auth_ok, current_broker,
|
||||||
|
COALESCE(brightness, 128) AS brightness,
|
||||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||||
FROM contadores
|
FROM contadores
|
||||||
WHERE blocked=0 OR auth_ok=1
|
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>ESTADO</th>
|
||||||
<th>BROKER</th>
|
<th>BROKER</th>
|
||||||
<th>ÚLTIMO</th>
|
<th>ÚLTIMO</th>
|
||||||
|
<th>BRILHO</th>
|
||||||
<th>AÇÕES</th>
|
<th>AÇÕES</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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-warning text-dark">RECOVERY</span>'
|
||||||
: '<span class="badge bg-primary">PRIMARY</span>';
|
: '<span class="badge bg-primary">PRIMARY</span>';
|
||||||
?>
|
?>
|
||||||
|
<?php $bri = (int)($r['brightness'] ?? 128); ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold"><?= htmlspecialchars($r['uid']) ?></td>
|
<td class="fw-bold"><?= htmlspecialchars($r['uid']) ?></td>
|
||||||
<td><?= htmlspecialchars($r['note'] ?? '') ?></td>
|
<td><?= htmlspecialchars($r['note'] ?? '') ?></td>
|
||||||
<td><?= $state ?></td>
|
<td><?= $state ?></td>
|
||||||
<td><?= $broker ?></td>
|
<td><?= $broker ?></td>
|
||||||
<td><?= $lastSeen ?></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>
|
<td>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="?toggle=<?= urlencode($r['uid']) ?>"
|
<a href="?toggle=<?= urlencode($r['uid']) ?>"
|
||||||
@ -262,5 +279,20 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,33 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
header("Content-Type: application/json; charset=utf-8");
|
header("Content-Type: application/json; charset=utf-8");
|
||||||
ini_set('display_errors', '1');
|
ini_set('display_errors', '1');
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$cfg = @include __DIR__ . "/config_db.php";
|
$cfg = @include __DIR__ . "/config_db.php";
|
||||||
if (!is_array($cfg)) {
|
if (!is_array($cfg)) {
|
||||||
throw new RuntimeException("Config DB inválida ou em falta (config_db.php).");
|
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']);
|
$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 = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
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 = $pdo->prepare("SELECT token_plain FROM api_tokens WHERE label = ? AND revoked = 0 ORDER BY id DESC LIMIT 1");
|
||||||
$stmt->execute(['matrix-admin']);
|
$stmt->execute(['matrix-admin']);
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
if (!$row || empty($row['token_plain'])) {
|
if (!$row || empty($row['token_plain'])) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(["error" => "Token não encontrado"]);
|
echo json_encode(["error" => "Token não encontrado"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(["token" => $row["token_plain"]]);
|
echo json_encode(["token" => $row["token_plain"]]);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(["error" => $e->getMessage()]);
|
echo json_encode(["error" => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|||||||
312
historico.php
312
historico.php
@ -1,156 +1,156 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . "/api/protecao.php";
|
require_once __DIR__ . "/api/protecao.php";
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Se pediu para limpar pendentes
|
// Se pediu para limpar pendentes
|
||||||
if (isset($_POST['clear_pending'])) {
|
if (isset($_POST['clear_pending'])) {
|
||||||
$pdo->exec("DELETE FROM comandos WHERE status = 'pending'");
|
$pdo->exec("DELETE FROM comandos WHERE status = 'pending'");
|
||||||
$_SESSION['flash'] = "🧹 Todos os comandos pendentes foram removidos!";
|
$_SESSION['flash'] = "🧹 Todos os comandos pendentes foram removidos!";
|
||||||
header("Location: historico.php");
|
header("Location: historico.php");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
$where = [];
|
$where = [];
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
if (!empty($_GET['uid'])) {
|
if (!empty($_GET['uid'])) {
|
||||||
$where[] = "c.uid = ?";
|
$where[] = "c.uid = ?";
|
||||||
$params[] = $_GET['uid'];
|
$params[] = $_GET['uid'];
|
||||||
}
|
}
|
||||||
if (!empty($_GET['status'])) {
|
if (!empty($_GET['status'])) {
|
||||||
$where[] = "c.status = ?";
|
$where[] = "c.status = ?";
|
||||||
$params[] = $_GET['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,
|
$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
|
r.payload AS resposta, r.ts AS resposta_at
|
||||||
FROM comandos c
|
FROM comandos c
|
||||||
LEFT JOIN respostas r ON r.uid = c.uid
|
LEFT JOIN respostas r ON r.uid = c.uid
|
||||||
" . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
|
" . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
|
||||||
ORDER BY c.queued_at DESC
|
ORDER BY c.queued_at DESC
|
||||||
LIMIT 50";
|
LIMIT 50";
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$comandos = $stmt->fetchAll();
|
$comandos = $stmt->fetchAll();
|
||||||
|
|
||||||
$devices = $pdo->query("SELECT DISTINCT uid FROM devices ORDER BY uid ASC")->fetchAll();
|
$devices = $pdo->query("SELECT DISTINCT uid FROM devices ORDER BY uid ASC")->fetchAll();
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
die("Erro: " . $e->getMessage());
|
die("Erro: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="pt">
|
<html lang="pt">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Histórico de Comandos</title>
|
<title>Histórico de Comandos</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body { background-color: #121212; color: #eee; }
|
body { background-color: #121212; color: #eee; }
|
||||||
.card, .table { background-color: #1e1e1e; color: #ddd; }
|
.card, .table { background-color: #1e1e1e; color: #ddd; }
|
||||||
.table thead { background-color: #333; }
|
.table thead { background-color: #333; }
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
// Auto-refresh a cada 5 segundos
|
// Auto-refresh a cada 5 segundos
|
||||||
setTimeout(() => location.reload(), 5000);
|
setTimeout(() => location.reload(), 5000);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="p-4">
|
<body class="p-4">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h2 class="mb-4 text-info">📜 Histórico de Comandos</h2>
|
<h2 class="mb-4 text-info">📜 Histórico de Comandos</h2>
|
||||||
|
|
||||||
<?php if (!empty($_SESSION['flash'])): ?>
|
<?php if (!empty($_SESSION['flash'])): ?>
|
||||||
<div class="alert alert-success"><?= $_SESSION['flash']; unset($_SESSION['flash']); ?></div>
|
<div class="alert alert-success"><?= $_SESSION['flash']; unset($_SESSION['flash']); ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Botão para limpar pendentes -->
|
<!-- Botão para limpar pendentes -->
|
||||||
<form method="POST" class="mb-3">
|
<form method="POST" class="mb-3">
|
||||||
<button type="submit" name="clear_pending" class="btn btn-danger">
|
<button type="submit" name="clear_pending" class="btn btn-danger">
|
||||||
🧹 Limpar Pendentes
|
🧹 Limpar Pendentes
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<form method="GET" class="row g-3 mb-4">
|
<form method="GET" class="row g-3 mb-4">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<select name="uid" class="form-select">
|
<select name="uid" class="form-select">
|
||||||
<option value="">-- Todos os dispositivos --</option>
|
<option value="">-- Todos os dispositivos --</option>
|
||||||
<?php foreach ($devices as $d): ?>
|
<?php foreach ($devices as $d): ?>
|
||||||
<option value="<?= htmlspecialchars($d['uid']) ?>" <?= (($_GET['uid'] ?? '') === $d['uid']) ? 'selected' : '' ?>>
|
<option value="<?= htmlspecialchars($d['uid']) ?>" <?= (($_GET['uid'] ?? '') === $d['uid']) ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($d['uid']) ?>
|
<?= htmlspecialchars($d['uid']) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<select name="status" class="form-select">
|
<select name="status" class="form-select">
|
||||||
<option value="">-- Todos os status --</option>
|
<option value="">-- Todos os status --</option>
|
||||||
<?php foreach (['pending','sent','done','failed'] as $s): ?>
|
<?php foreach (['pending','sent','done','failed'] as $s): ?>
|
||||||
<option value="<?= $s ?>" <?= (($_GET['status'] ?? '') === $s) ? 'selected' : '' ?>>
|
<option value="<?= $s ?>" <?= (($_GET['status'] ?? '') === $s) ? 'selected' : '' ?>>
|
||||||
<?= ucfirst($s) ?>
|
<?= ucfirst($s) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<button class="btn btn-primary w-100">🔎 Filtrar</button>
|
<button class="btn btn-primary w-100">🔎 Filtrar</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Tabela -->
|
<!-- Tabela -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-dark table-hover shadow-sm">
|
<table class="table table-dark table-hover shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Dispositivo</th>
|
<th>Dispositivo</th>
|
||||||
<th>Comando</th>
|
<th>Comando</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Enfileirado</th>
|
<th>Enfileirado</th>
|
||||||
<th>Enviado</th>
|
<th>Enviado</th>
|
||||||
<th>ACK</th>
|
<th>ACK</th>
|
||||||
<th>Erro</th>
|
<th>Erro</th>
|
||||||
<th>Resposta</th>
|
<th>Resposta</th>
|
||||||
<th>Resp. At</th>
|
<th>Resp. At</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($comandos as $c): ?>
|
<?php foreach ($comandos as $c): ?>
|
||||||
<?php
|
<?php
|
||||||
switch ($c['status']) {
|
switch ($c['status']) {
|
||||||
case 'pending': $badge = 'warning'; break;
|
case 'pending': $badge = 'warning'; break;
|
||||||
case 'sent': $badge = 'info'; break;
|
case 'sent': $badge = 'info'; break;
|
||||||
case 'done': $badge = 'success'; break;
|
case 'done': $badge = 'success'; break;
|
||||||
case 'failed': $badge = 'danger'; break;
|
case 'failed': $badge = 'danger'; break;
|
||||||
default: $badge = 'secondary'; break;
|
default: $badge = 'secondary'; break;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= $c['id'] ?></td>
|
<td><?= $c['id'] ?></td>
|
||||||
<td><?= htmlspecialchars($c['uid']) ?></td>
|
<td><?= htmlspecialchars($c['uid']) ?></td>
|
||||||
<td><?= htmlspecialchars($c['cmd']) ?></td>
|
<td><?= htmlspecialchars($c['cmd']) ?></td>
|
||||||
<td><span class="badge bg-<?= $badge ?>"><?= $c['status'] ?></span></td>
|
<td><span class="badge bg-<?= $badge ?>"><?= $c['status'] ?></span></td>
|
||||||
<td><?= $c['queued_at'] ?></td>
|
<td><?= $c['queued_at'] ?></td>
|
||||||
<td><?= $c['sent_at'] ?></td>
|
<td><?= $c['sent_at'] ?></td>
|
||||||
<td><?= $c['ack_ts'] ?></td>
|
<td><?= $c['ack_ts'] ?></td>
|
||||||
<td><?= htmlspecialchars($c['error'] ?? '') ?></td>
|
<td><?= htmlspecialchars($c['error'] ?? '') ?></td>
|
||||||
<td><?= htmlspecialchars($c['resposta'] ?? '') ?></td>
|
<td><?= htmlspecialchars($c['resposta'] ?? '') ?></td>
|
||||||
<td><?= $c['resposta_at'] ?></td>
|
<td><?= $c['resposta_at'] ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
|
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
102
homeserver.yaml
102
homeserver.yaml
@ -1,51 +1,51 @@
|
|||||||
server_name: "matrix.xupas.mywire.org"
|
server_name: "matrix.xupas.mywire.org"
|
||||||
pid_file: /data/homeserver.pid
|
pid_file: /data/homeserver.pid
|
||||||
listeners:
|
listeners:
|
||||||
- port: 8008
|
- port: 8008
|
||||||
tls: false
|
tls: false
|
||||||
type: http
|
type: http
|
||||||
x_forwarded: true
|
x_forwarded: true
|
||||||
resources:
|
resources:
|
||||||
- names: [client, federation]
|
- names: [client, federation]
|
||||||
compress: false
|
compress: false
|
||||||
|
|
||||||
database:
|
database:
|
||||||
name: psycopg2
|
name: psycopg2
|
||||||
args:
|
args:
|
||||||
user: synapse
|
user: synapse
|
||||||
password: synapse
|
password: synapse
|
||||||
database: synapse
|
database: synapse
|
||||||
host: matrix-postgres-1
|
host: matrix-postgres-1
|
||||||
cp_min: 5
|
cp_min: 5
|
||||||
cp_max: 10
|
cp_max: 10
|
||||||
allow_unsafe_locale: true
|
allow_unsafe_locale: true
|
||||||
|
|
||||||
experimental_features:
|
experimental_features:
|
||||||
msc3202_enabled: true
|
msc3202_enabled: true
|
||||||
msc3077_enabled: true
|
msc3077_enabled: true
|
||||||
msc3401_enabled: true
|
msc3401_enabled: true
|
||||||
msc3391_enabled: true
|
msc3391_enabled: true
|
||||||
msc3890_enabled: true
|
msc3890_enabled: true
|
||||||
msc4153_enabled: true
|
msc4153_enabled: true
|
||||||
|
|
||||||
turn_uris:
|
turn_uris:
|
||||||
- "turn:192.168.10.253:5349?transport=udp"
|
- "turn:192.168.10.253:5349?transport=udp"
|
||||||
- "turn:192.168.10.253:5349?transport=tcp"
|
- "turn:192.168.10.253:5349?transport=tcp"
|
||||||
turn_shared_secret: "c246729fea23718edc2bf08726f6822f6732b1bde3c2e062194fc5e6758b0cf7"
|
turn_shared_secret: "c246729fea23718edc2bf08726f6822f6732b1bde3c2e062194fc5e6758b0cf7"
|
||||||
turn_user_lifetime: 86400000
|
turn_user_lifetime: 86400000
|
||||||
turn_allow_guests: true
|
turn_allow_guests: true
|
||||||
|
|
||||||
log_config: "/data/matrix.xupas.mywire.org.log.config"
|
log_config: "/data/matrix.xupas.mywire.org.log.config"
|
||||||
media_store_path: /data/media_store
|
media_store_path: /data/media_store
|
||||||
registration_shared_secret: "c#PmLj2F,GAR,xFWL.uw-Gb0TF47oKD^Nb@a~h~D@WRVBHdo*y"
|
registration_shared_secret: "c#PmLj2F,GAR,xFWL.uw-Gb0TF47oKD^Nb@a~h~D@WRVBHdo*y"
|
||||||
report_stats: true
|
report_stats: true
|
||||||
macaroon_secret_key: "5XLRSbZP&ij+Dh=SC4^hUL2SjleMkIS69=uPsS&n6D8aLqJGqA"
|
macaroon_secret_key: "5XLRSbZP&ij+Dh=SC4^hUL2SjleMkIS69=uPsS&n6D8aLqJGqA"
|
||||||
form_secret: "ht7o4U6Ue@S7HBDVVJczKH*4h_gBkADg@ursU0Ye*RBIn7R8w8"
|
form_secret: "ht7o4U6Ue@S7HBDVVJczKH*4h_gBkADg@ursU0Ye*RBIn7R8w8"
|
||||||
signing_key_path: "/data/matrix.xupas.mywire.org.signing.key"
|
signing_key_path: "/data/matrix.xupas.mywire.org.signing.key"
|
||||||
trusted_key_servers:
|
trusted_key_servers:
|
||||||
- server_name: "matrix.org"
|
- server_name: "matrix.org"
|
||||||
|
|
||||||
suppress_key_server_warning: true
|
suppress_key_server_warning: true
|
||||||
enable_registration: false
|
enable_registration: false
|
||||||
enable_registration_without_verification: false
|
enable_registration_without_verification: false
|
||||||
|
|
||||||
|
|||||||
558
matrix_admin.php
558
matrix_admin.php
@ -1,279 +1,279 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
ini_set('display_errors', '1');
|
ini_set('display_errors', '1');
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// ⚙️ CONFIGURAÇÃO
|
// ⚙️ CONFIGURAÇÃO
|
||||||
// ==============================
|
// ==============================
|
||||||
$MATRIX_BASE = "https://matrix.xupas.mywire.org";
|
$MATRIX_BASE = "https://matrix.xupas.mywire.org";
|
||||||
$SERVER_NAME = "matrix.xupas.mywire.org";
|
$SERVER_NAME = "matrix.xupas.mywire.org";
|
||||||
$TOKEN_FILE = "/var/www/secure/matrix_token.txt";
|
$TOKEN_FILE = "/var/www/secure/matrix_token.txt";
|
||||||
|
|
||||||
$ADMIN_TOKEN = is_readable($TOKEN_FILE) ? trim(file_get_contents($TOKEN_FILE)) : '';
|
$ADMIN_TOKEN = is_readable($TOKEN_FILE) ? trim(file_get_contents($TOKEN_FILE)) : '';
|
||||||
|
|
||||||
$DEBUG_LOG = "/tmp/matrix_debug.log";
|
$DEBUG_LOG = "/tmp/matrix_debug.log";
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// 📝 DEBUG HELPER
|
// 📝 DEBUG HELPER
|
||||||
// ==============================
|
// ==============================
|
||||||
function dbg($txt) {
|
function dbg($txt) {
|
||||||
global $DEBUG_LOG;
|
global $DEBUG_LOG;
|
||||||
file_put_contents($DEBUG_LOG, "[".date('Y-m-d H:i:s')."] $txt\n", FILE_APPEND);
|
file_put_contents($DEBUG_LOG, "[".date('Y-m-d H:i:s')."] $txt\n", FILE_APPEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// 🔧 API CALL
|
// 🔧 API CALL
|
||||||
// ==============================
|
// ==============================
|
||||||
function call_api(string $method, string $url, ?array $data = null, ?string $token = null): array {
|
function call_api(string $method, string $url, ?array $data = null, ?string $token = null): array {
|
||||||
dbg("API CALL: $method $url DATA=" . json_encode($data));
|
dbg("API CALL: $method $url DATA=" . json_encode($data));
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
$headers = ['Content-Type: application/json'];
|
$headers = ['Content-Type: application/json'];
|
||||||
if ($token) $headers[] = "Authorization: Bearer $token";
|
if ($token) $headers[] = "Authorization: Bearer $token";
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_CUSTOMREQUEST => $method,
|
CURLOPT_CUSTOMREQUEST => $method,
|
||||||
CURLOPT_HTTPHEADER => $headers,
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
CURLOPT_TIMEOUT => 60,
|
CURLOPT_TIMEOUT => 60,
|
||||||
]);
|
]);
|
||||||
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
$resp = curl_exec($ch);
|
$resp = curl_exec($ch);
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
dbg("API RESP: HTTP=$code BODY=$resp");
|
dbg("API RESP: HTTP=$code BODY=$resp");
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
return [$code, $resp];
|
return [$code, $resp];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// 🧼 USERNAME CLEANER FIX
|
// 🧼 USERNAME CLEANER FIX
|
||||||
// ==============================
|
// ==============================
|
||||||
function full_user(string $name, string $server): string {
|
function full_user(string $name, string $server): string {
|
||||||
dbg("RAW POST USER=[".$name."] HEX=[".bin2hex($name)."]");
|
dbg("RAW POST USER=[".$name."] HEX=[".bin2hex($name)."]");
|
||||||
$clean = trim($name);
|
$clean = trim($name);
|
||||||
$clean = preg_replace('/[\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]/u', '', $clean);
|
$clean = preg_replace('/[\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]/u', '', $clean);
|
||||||
dbg("CLEAN USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
dbg("CLEAN USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
||||||
if (preg_match('/^@[a-z0-9=_\-\.\/\+]+:[a-z0-9\.\-]+$/i', $clean)) return $clean;
|
if (preg_match('/^@[a-z0-9=_\-\.\/\+]+:[a-z0-9\.\-]+$/i', $clean)) return $clean;
|
||||||
if ($clean === '') return '';
|
if ($clean === '') return '';
|
||||||
if ($clean[0] !== '@') $clean = '@' . $clean;
|
if ($clean[0] !== '@') $clean = '@' . $clean;
|
||||||
if (!str_contains($clean, ':')) $clean .= ':' . $server;
|
if (!str_contains($clean, ':')) $clean .= ':' . $server;
|
||||||
dbg("FINAL USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
dbg("FINAL USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
||||||
return $clean;
|
return $clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// 🚦 PROCESSAMENTO DE AÇÕES
|
// 🚦 PROCESSAMENTO DE AÇÕES
|
||||||
// ==============================
|
// ==============================
|
||||||
$action = $_POST['action'] ?? null;
|
$action = $_POST['action'] ?? null;
|
||||||
$msg = '';
|
$msg = '';
|
||||||
|
|
||||||
if ($action && $ADMIN_TOKEN) {
|
if ($action && $ADMIN_TOKEN) {
|
||||||
|
|
||||||
dbg("=== ACTION: $action ===");
|
dbg("=== ACTION: $action ===");
|
||||||
dbg("POST DATA: " . json_encode($_POST));
|
dbg("POST DATA: " . json_encode($_POST));
|
||||||
|
|
||||||
// criar utilizador
|
// criar utilizador
|
||||||
if ($action === 'add_user') {
|
if ($action === 'add_user') {
|
||||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||||
$pass = trim($_POST['pass']);
|
$pass = trim($_POST['pass']);
|
||||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||||
"password" => $pass,
|
"password" => $pass,
|
||||||
"deactivated" => false,
|
"deactivated" => false,
|
||||||
"admin" => false
|
"admin" => false
|
||||||
], $ADMIN_TOKEN);
|
], $ADMIN_TOKEN);
|
||||||
$msg = ($code == 200 || $code == 201) ? "✅ Criado: $user" : "❌ Erro [$code]: $resp";
|
$msg = ($code == 200 || $code == 201) ? "✅ Criado: $user" : "❌ Erro [$code]: $resp";
|
||||||
}
|
}
|
||||||
|
|
||||||
// alterar password
|
// alterar password
|
||||||
if ($action === 'change_pass') {
|
if ($action === 'change_pass') {
|
||||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||||
$pass = trim($_POST['pass']);
|
$pass = trim($_POST['pass']);
|
||||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/reset_password/" . urlencode($user), [
|
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/reset_password/" . urlencode($user), [
|
||||||
"new_password" => $pass
|
"new_password" => $pass
|
||||||
], $ADMIN_TOKEN);
|
], $ADMIN_TOKEN);
|
||||||
$msg = ($code == 200) ? "🔑 Senha alterada para $user" : "❌ Erro [$code]: $resp";
|
$msg = ($code == 200) ? "🔑 Senha alterada para $user" : "❌ Erro [$code]: $resp";
|
||||||
}
|
}
|
||||||
|
|
||||||
// admin / unadmin
|
// admin / unadmin
|
||||||
if ($action === 'make_admin' || $action === 'revoke_admin') {
|
if ($action === 'make_admin' || $action === 'revoke_admin') {
|
||||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||||
$is_admin = $action === 'make_admin';
|
$is_admin = $action === 'make_admin';
|
||||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||||
"admin" => $is_admin
|
"admin" => $is_admin
|
||||||
], $ADMIN_TOKEN);
|
], $ADMIN_TOKEN);
|
||||||
$msg = ($code == 200)
|
$msg = ($code == 200)
|
||||||
? ($is_admin ? "⬆️ $user agora é admin" : "⬇️ $user deixou de ser admin")
|
? ($is_admin ? "⬆️ $user agora é admin" : "⬇️ $user deixou de ser admin")
|
||||||
: "❌ Erro [$code]: $resp";
|
: "❌ Erro [$code]: $resp";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ativar / desativar
|
// ativar / desativar
|
||||||
if ($action === 'deactivate_user' || $action === 'reactivate_user') {
|
if ($action === 'deactivate_user' || $action === 'reactivate_user') {
|
||||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||||
$active = $action === 'reactivate_user';
|
$active = $action === 'reactivate_user';
|
||||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||||
"deactivated" => !$active
|
"deactivated" => !$active
|
||||||
], $ADMIN_TOKEN);
|
], $ADMIN_TOKEN);
|
||||||
$msg = ($code == 200)
|
$msg = ($code == 200)
|
||||||
? ($active ? "♻️ $user reativado" : "🚫 $user desativado")
|
? ($active ? "♻️ $user reativado" : "🚫 $user desativado")
|
||||||
: "❌ Erro [$code]: $resp";
|
: "❌ Erro [$code]: $resp";
|
||||||
}
|
}
|
||||||
|
|
||||||
// apagar user
|
// apagar user
|
||||||
if ($action === 'delete_user') {
|
if ($action === 'delete_user') {
|
||||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/deactivate/" . urlencode($user), [
|
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/deactivate/" . urlencode($user), [
|
||||||
"erase" => true
|
"erase" => true
|
||||||
], $ADMIN_TOKEN);
|
], $ADMIN_TOKEN);
|
||||||
$msg = ($code == 200 || $code == 204)
|
$msg = ($code == 200 || $code == 204)
|
||||||
? "🗑️ $user removido permanentemente"
|
? "🗑️ $user removido permanentemente"
|
||||||
: "❌ Erro [$code]: $resp";
|
: "❌ Erro [$code]: $resp";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NOVA: apagar mensagens do user (mantendo o user)
|
// ✅ NOVA: apagar mensagens do user (mantendo o user)
|
||||||
if ($action === 'erase_messages') {
|
if ($action === 'erase_messages') {
|
||||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/user/$user/rooms", null, $ADMIN_TOKEN);
|
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/user/$user/rooms", null, $ADMIN_TOKEN);
|
||||||
$rooms = json_decode($resp, true)['rooms'] ?? [];
|
$rooms = json_decode($resp, true)['rooms'] ?? [];
|
||||||
$errors = [];
|
$errors = [];
|
||||||
foreach ($rooms as $room) {
|
foreach ($rooms as $room) {
|
||||||
[$c, $r] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/purge_history/" . urlencode($room['room_id']), [
|
[$c, $r] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/purge_history/" . urlencode($room['room_id']), [
|
||||||
"delete_local_events" => true,
|
"delete_local_events" => true,
|
||||||
"reason" => "Apagadas via painel"
|
"reason" => "Apagadas via painel"
|
||||||
], $ADMIN_TOKEN);
|
], $ADMIN_TOKEN);
|
||||||
if ($c != 200) $errors[] = "$room[room_id]: $r";
|
if ($c != 200) $errors[] = "$room[room_id]: $r";
|
||||||
}
|
}
|
||||||
$msg = empty($errors) ? "🧹 Mensagens de $user apagadas" : "❌ Erros: " . implode("; ", $errors);
|
$msg = empty($errors) ? "🧹 Mensagens de $user apagadas" : "❌ Erros: " . implode("; ", $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NOVA: limpar metadados gerais
|
// ✅ NOVA: limpar metadados gerais
|
||||||
if ($action === 'clear_metadata') {
|
if ($action === 'clear_metadata') {
|
||||||
@unlink($DEBUG_LOG);
|
@unlink($DEBUG_LOG);
|
||||||
// aqui podes adicionar mais caminhos de logs/metadados
|
// aqui podes adicionar mais caminhos de logs/metadados
|
||||||
$msg = "⚡ Metadados limpos!";
|
$msg = "⚡ Metadados limpos!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// 📋 LISTAGEM USERS
|
// 📋 LISTAGEM USERS
|
||||||
// ==============================
|
// ==============================
|
||||||
$users = [];
|
$users = [];
|
||||||
if ($ADMIN_TOKEN) {
|
if ($ADMIN_TOKEN) {
|
||||||
$next = null;
|
$next = null;
|
||||||
do {
|
do {
|
||||||
$url = "$MATRIX_BASE/_synapse/admin/v2/users?limit=100";
|
$url = "$MATRIX_BASE/_synapse/admin/v2/users?limit=100";
|
||||||
if ($next) $url .= "&from=" . urlencode($next);
|
if ($next) $url .= "&from=" . urlencode($next);
|
||||||
[$code, $resp] = call_api("GET", $url, null, $ADMIN_TOKEN);
|
[$code, $resp] = call_api("GET", $url, null, $ADMIN_TOKEN);
|
||||||
if ($code != 200) { dbg("ERRO AO LER USERS: HTTP=$code BODY=$resp"); break; }
|
if ($code != 200) { dbg("ERRO AO LER USERS: HTTP=$code BODY=$resp"); break; }
|
||||||
$data = json_decode($resp, true);
|
$data = json_decode($resp, true);
|
||||||
$chunk = $data['users'] ?? $data['results'] ?? [];
|
$chunk = $data['users'] ?? $data['results'] ?? [];
|
||||||
$users = array_merge($users, $chunk);
|
$users = array_merge($users, $chunk);
|
||||||
$next = $data['next_token'] ?? null;
|
$next = $data['next_token'] ?? null;
|
||||||
} while ($next);
|
} while ($next);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="pt">
|
<html lang="pt">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Matrix Admin - Xupas</title>
|
<title>Matrix Admin - Xupas</title>
|
||||||
<style>
|
<style>
|
||||||
body {background:#0e0e10;color:#eee;font-family:Segoe UI,Arial;margin:20px;}
|
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;}
|
.box{background:#1a1a1d;border:1px solid #333;border-radius:10px;padding:12px;margin-top:10px;}
|
||||||
h2{color:#3fa9f5;}
|
h2{color:#3fa9f5;}
|
||||||
input,select,button{background:#202124;color:#eee;border:1px solid #333;border-radius:6px;padding:6px 8px;margin:2px;}
|
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;}
|
button:hover{background:#3fa9f5;color:#000;}
|
||||||
table{border-collapse:collapse;width:100%;margin-top:10px;}
|
table{border-collapse:collapse;width:100%;margin-top:10px;}
|
||||||
th,td{border:1px solid #333;padding:6px;}
|
th,td{border:1px solid #333;padding:6px;}
|
||||||
tr:nth-child(even){background:#171717;}
|
tr:nth-child(even){background:#171717;}
|
||||||
.msg{padding:8px;margin:8px 0;border-radius:6px;}
|
.msg{padding:8px;margin:8px 0;border-radius:6px;}
|
||||||
.ok{background:#12391f;color:#4caf50;}
|
.ok{background:#12391f;color:#4caf50;}
|
||||||
.err{background:#3a1515;color:#ffb3b3;}
|
.err{background:#3a1515;color:#ffb3b3;}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>💻 Matrix Admin - Xupas</h2>
|
<h2>💻 Matrix Admin - Xupas</h2>
|
||||||
|
|
||||||
<?php if ($msg): ?>
|
<?php if ($msg): ?>
|
||||||
<div class="msg <?=str_starts_with($msg,'❌')?'err':'ok'?>"><?=htmlspecialchars($msg)?></div>
|
<div class="msg <?=str_starts_with($msg,'❌')?'err':'ok'?>"><?=htmlspecialchars($msg)?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h3>➕ Criar Utilizador</h3>
|
<h3>➕ Criar Utilizador</h3>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="action" value="add_user">
|
<input type="hidden" name="action" value="add_user">
|
||||||
<input type="text" name="user" placeholder="nome" required>
|
<input type="text" name="user" placeholder="nome" required>
|
||||||
<input type="password" name="pass" placeholder="senha" required>
|
<input type="password" name="pass" placeholder="senha" required>
|
||||||
<button>Criar</button>
|
<button>Criar</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h3>👥 Utilizadores</h3>
|
<h3>👥 Utilizadores</h3>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Admin</th>
|
<th>Admin</th>
|
||||||
<th>Ativo</th>
|
<th>Ativo</th>
|
||||||
<th>Último Login</th>
|
<th>Último Login</th>
|
||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
<?php foreach($users as $u):
|
<?php foreach($users as $u):
|
||||||
$n = $u['name'] ?? $u['user_id'] ?? '—';
|
$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)) : '—';
|
$seen = isset($u['last_seen_ts']) ? date('Y-m-d H:i:s', (int)($u['last_seen_ts'] / 1000)) : '—';
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?=$n?></td>
|
<td><?=$n?></td>
|
||||||
<td><?=!empty($u['admin'])?'🟢':'⚫'?></td>
|
<td><?=!empty($u['admin'])?'🟢':'⚫'?></td>
|
||||||
<td><?=!empty($u['deactivated'])?'❌':'✅'?></td>
|
<td><?=!empty($u['deactivated'])?'❌':'✅'?></td>
|
||||||
<td><?=$seen?></td>
|
<td><?=$seen?></td>
|
||||||
<td>
|
<td>
|
||||||
<!-- Alterar senha -->
|
<!-- Alterar senha -->
|
||||||
<form method="post" style="display:inline">
|
<form method="post" style="display:inline">
|
||||||
<input type="hidden" name="action" value="change_pass">
|
<input type="hidden" name="action" value="change_pass">
|
||||||
<input type="hidden" name="user" value="<?=$n?>">
|
<input type="hidden" name="user" value="<?=$n?>">
|
||||||
<input type="password" name="pass" placeholder="nova" style="width:90px">
|
<input type="password" name="pass" placeholder="nova" style="width:90px">
|
||||||
<button>🔑</button>
|
<button>🔑</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Tornar admin / remover admin -->
|
<!-- Tornar admin / remover admin -->
|
||||||
<form method="post" style="display:inline">
|
<form method="post" style="display:inline">
|
||||||
<input type="hidden" name="action" value="<?=!empty($u['admin'])?'revoke_admin':'make_admin'?>">
|
<input type="hidden" name="action" value="<?=!empty($u['admin'])?'revoke_admin':'make_admin'?>">
|
||||||
<input type="hidden" name="user" value="<?=$n?>">
|
<input type="hidden" name="user" value="<?=$n?>">
|
||||||
<button><?=!empty($u['admin'])?'⬇️':'⬆️'?></button>
|
<button><?=!empty($u['admin'])?'⬇️':'⬆️'?></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Ativar / desativar -->
|
<!-- Ativar / desativar -->
|
||||||
<form method="post" style="display:inline">
|
<form method="post" style="display:inline">
|
||||||
<input type="hidden" name="action" value="<?=!empty($u['deactivated'])?'reactivate_user':'deactivate_user'?>">
|
<input type="hidden" name="action" value="<?=!empty($u['deactivated'])?'reactivate_user':'deactivate_user'?>">
|
||||||
<input type="hidden" name="user" value="<?=$n?>">
|
<input type="hidden" name="user" value="<?=$n?>">
|
||||||
<button><?=!empty($u['deactivated'])?'♻️':'🚫'?></button>
|
<button><?=!empty($u['deactivated'])?'♻️':'🚫'?></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Apagar user -->
|
<!-- Apagar user -->
|
||||||
<form method="post" style="display:inline" onsubmit="return confirm('Apagar <?=$n?>?');">
|
<form method="post" style="display:inline" onsubmit="return confirm('Apagar <?=$n?>?');">
|
||||||
<input type="hidden" name="action" value="delete_user">
|
<input type="hidden" name="action" value="delete_user">
|
||||||
<input type="hidden" name="user" value="<?=$n?>">
|
<input type="hidden" name="user" value="<?=$n?>">
|
||||||
<button>🗑️</button>
|
<button>🗑️</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- NOVO: apagar mensagens do user -->
|
<!-- NOVO: apagar mensagens do user -->
|
||||||
<form method="post" style="display:inline" onsubmit="return confirm('Apagar TODAS mensagens de <?=$n?>?');">
|
<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="action" value="erase_messages">
|
||||||
<input type="hidden" name="user" value="<?=$n?>">
|
<input type="hidden" name="user" value="<?=$n?>">
|
||||||
<button>🧹</button>
|
<button>🧹</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- NOVO: limpar metadados gerais -->
|
<!-- NOVO: limpar metadados gerais -->
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h3>⚡ Limpar metadados gerais</h3>
|
<h3>⚡ Limpar metadados gerais</h3>
|
||||||
<form method="post" onsubmit="return confirm('⚠️ Isto apagará logs e metadados, mas não afetará utilizadores ou mensagens. Continuar?');">
|
<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">
|
<input type="hidden" name="action" value="clear_metadata">
|
||||||
<button>🗑️ Limpar tudo</button>
|
<button>🗑️ Limpar tudo</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
211
proxmox.php
Normal file
211
proxmox.php
Normal 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';
|
||||||
62
publish.php
62
publish.php
@ -1,31 +1,31 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__.'/vendor/autoload.php';
|
require __DIR__.'/vendor/autoload.php';
|
||||||
use PhpMqtt\Client\MqttClient;
|
use PhpMqtt\Client\MqttClient;
|
||||||
use PhpMqtt\Client\ConnectionSettings;
|
use PhpMqtt\Client\ConnectionSettings;
|
||||||
|
|
||||||
$config = require __DIR__.'/config.php';
|
$config = require __DIR__.'/config.php';
|
||||||
|
|
||||||
$topic = $_POST['topic'] ?? $_GET['topic'] ?? null;
|
$topic = $_POST['topic'] ?? $_GET['topic'] ?? null;
|
||||||
$msg = $_POST['msg'] ?? $_GET['msg'] ?? null;
|
$msg = $_POST['msg'] ?? $_GET['msg'] ?? null;
|
||||||
$qos = (int)($_POST['qos'] ?? 0);
|
$qos = (int)($_POST['qos'] ?? 0);
|
||||||
$retain = (bool)($_POST['retain'] ?? false);
|
$retain = (bool)($_POST['retain'] ?? false);
|
||||||
|
|
||||||
if (!$topic || $msg === null) {
|
if (!$topic || $msg === null) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
die('missing topic or msg');
|
die('missing topic or msg');
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings = (new ConnectionSettings)
|
$settings = (new ConnectionSettings)
|
||||||
->setUsername($config['username'])
|
->setUsername($config['username'])
|
||||||
->setPassword($config['password'])
|
->setPassword($config['password'])
|
||||||
->setKeepAliveInterval($config['keepalive'])
|
->setKeepAliveInterval($config['keepalive'])
|
||||||
->setConnectTimeout($config['timeout'])
|
->setConnectTimeout($config['timeout'])
|
||||||
->setUseTls(false); // muda para true quando fores usar 8883/TLS
|
->setUseTls(false); // muda para true quando fores usar 8883/TLS
|
||||||
|
|
||||||
$client = new MqttClient($config['host'], $config['port'], $config['clientId']);
|
$client = new MqttClient($config['host'], $config['port'], $config['clientId']);
|
||||||
$client->connect($settings, $config['clean']);
|
$client->connect($settings, $config['clean']);
|
||||||
$client->publish($topic, $msg, $qos, $retain);
|
$client->publish($topic, $msg, $qos, $retain);
|
||||||
$client->disconnect();
|
$client->disconnect();
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode(['ok'=>true,'topic'=>$topic,'qos'=>$qos,'retain'=>$retain]);
|
echo json_encode(['ok'=>true,'topic'=>$topic,'qos'=>$qos,'retain'=>$retain]);
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . "/api/protecao.php";
|
require_once __DIR__ . "/api/protecao.php";
|
||||||
require __DIR__ . "/api/db.php";
|
require __DIR__ . "/api/db.php";
|
||||||
|
|
||||||
$uid = $_POST['uid'] ?? '';
|
$uid = $_POST['uid'] ?? '';
|
||||||
$note = $_POST['note'] ?? '';
|
$note = $_POST['note'] ?? '';
|
||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE contadores
|
UPDATE contadores
|
||||||
SET note = ?
|
SET note = ?
|
||||||
WHERE uid = ?
|
WHERE uid = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([$note, $uid]);
|
$stmt->execute([$note, $uid]);
|
||||||
|
|
||||||
header("Location: console.php");
|
header("Location: console.php");
|
||||||
@ -1,25 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
// ---------------- DB ----------------
|
// ---------------- DB ----------------
|
||||||
$dbHost = "localhost";
|
$dbHost = "localhost";
|
||||||
$dbName = "cagalhao";
|
$dbName = "cagalhao";
|
||||||
$dbUser = "master";
|
$dbUser = "master";
|
||||||
$dbPass = "master";
|
$dbPass = "master";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
||||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Erro DB: " . $e->getMessage());
|
die("Erro DB: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- INPUT ----------------
|
// ---------------- INPUT ----------------
|
||||||
$uid = $_GET['uid'] ?? '';
|
$uid = $_GET['uid'] ?? '';
|
||||||
|
|
||||||
if ($uid) {
|
if ($uid) {
|
||||||
$stmt = $db->prepare("INSERT INTO comandos (uid, cmd, params, status)
|
$stmt = $db->prepare("INSERT INTO comandos (uid, cmd, params, status)
|
||||||
VALUES (:uid, 'PING', '{}', 'pending')");
|
VALUES (:uid, 'PING', '{}', 'pending')");
|
||||||
$stmt->execute([':uid' => $uid]);
|
$stmt->execute([':uid' => $uid]);
|
||||||
// echo "PING enviado para {$uid}";
|
// echo "PING enviado para {$uid}";
|
||||||
} else {
|
} else {
|
||||||
echo "Falta o UID.";
|
echo "Falta o UID.";
|
||||||
}
|
}
|
||||||
|
|||||||
50
vendor/php-mqtt/client/.github/release.yml
vendored
50
vendor/php-mqtt/client/.github/release.yml
vendored
@ -1,25 +1,25 @@
|
|||||||
changelog:
|
changelog:
|
||||||
exclude:
|
exclude:
|
||||||
labels:
|
labels:
|
||||||
- ignore-for-release
|
- ignore-for-release
|
||||||
authors:
|
authors:
|
||||||
- octocat
|
- octocat
|
||||||
categories:
|
categories:
|
||||||
- title: Added
|
- title: Added
|
||||||
labels:
|
labels:
|
||||||
- enhancement
|
- enhancement
|
||||||
- title: Deprecated
|
- title: Deprecated
|
||||||
labels:
|
labels:
|
||||||
- deprecated
|
- deprecated
|
||||||
- title: Removed
|
- title: Removed
|
||||||
labels:
|
labels:
|
||||||
- removed
|
- removed
|
||||||
- title: Fixed
|
- title: Fixed
|
||||||
labels:
|
labels:
|
||||||
- bug
|
- bug
|
||||||
- title: Security
|
- title: Security
|
||||||
labels:
|
labels:
|
||||||
- security
|
- security
|
||||||
- title: Changed
|
- title: Changed
|
||||||
labels:
|
labels:
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
228
vpn.php
Normal file
228
vpn.php
Normal 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';
|
||||||
Loading…
Reference in New Issue
Block a user