commit-all-changes
This commit is contained in:
parent
0d8cf8a7e5
commit
bea0c5c200
@ -1,35 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . "/db.php";
|
||||
|
||||
$rows = $pdo->query("
|
||||
SELECT uid, note, entradas, saidas, online, blocked,
|
||||
current_broker, rssi, heap, uptime,
|
||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||
FROM contadores
|
||||
ORDER BY online DESC, uid ASC
|
||||
")->fetchAll();
|
||||
|
||||
// Totais
|
||||
$sumEntradas = 0;
|
||||
$sumSaidas = 0;
|
||||
$onlineCount = 0;
|
||||
$offlineCount = 0;
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$sumEntradas += (int)$r['entradas'];
|
||||
$sumSaidas += (int)$r['saidas'];
|
||||
if ($r['online'] == 1) $onlineCount++;
|
||||
else $offlineCount++;
|
||||
}
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
echo json_encode([
|
||||
"rows" => $rows,
|
||||
"online" => $onlineCount,
|
||||
"offline" => $offlineCount,
|
||||
"entradas" => $sumEntradas,
|
||||
"saidas" => $sumSaidas,
|
||||
]);
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . "/db.php";
|
||||
|
||||
$rows = $pdo->query("
|
||||
SELECT uid, note, entradas, saidas, online, blocked,
|
||||
current_broker, rssi, heap, uptime,
|
||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||
FROM contadores
|
||||
ORDER BY online DESC, uid ASC
|
||||
")->fetchAll();
|
||||
|
||||
// Totais
|
||||
$sumEntradas = 0;
|
||||
$sumSaidas = 0;
|
||||
$onlineCount = 0;
|
||||
$offlineCount = 0;
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$sumEntradas += (int)$r['entradas'];
|
||||
$sumSaidas += (int)$r['saidas'];
|
||||
if ($r['online'] == 1) $onlineCount++;
|
||||
else $offlineCount++;
|
||||
}
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
echo json_encode([
|
||||
"rows" => $rows,
|
||||
"online" => $onlineCount,
|
||||
"offline" => $offlineCount,
|
||||
"entradas" => $sumEntradas,
|
||||
"saidas" => $sumSaidas,
|
||||
]);
|
||||
|
||||
@ -50,29 +50,12 @@ $db = new PDO(
|
||||
]
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// RECOVERY MQTT
|
||||
// =====================================================
|
||||
|
||||
$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
|
||||
$recoveryPort = 8884;
|
||||
|
||||
$recoveryUser = 'recovery';
|
||||
$recoveryPass = 'recovery123';
|
||||
|
||||
// =====================================================
|
||||
// MAIN MQTT
|
||||
// =====================================================
|
||||
|
||||
$mainHost = 'mqtt.xupas.mywire.org';
|
||||
$mainPort = 8883;
|
||||
|
||||
// =====================================================
|
||||
// CERT
|
||||
// =====================================================
|
||||
|
||||
$certPath = '/etc/mosquitto/certs/chain.pem'; // cert Let's Encrypt do broker principal
|
||||
$certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
|
||||
// ─── migração: coluna auth_nonce_at (idempotente) ──────
|
||||
$db->exec("
|
||||
ALTER TABLE contadores
|
||||
ADD COLUMN IF NOT EXISTS
|
||||
auth_nonce_at DATETIME NULL DEFAULT NULL
|
||||
");
|
||||
|
||||
// =====================================================
|
||||
// GLOBAL SECRET
|
||||
@ -81,28 +64,92 @@ $certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
|
||||
$authSecret = 'K82A91F0';
|
||||
|
||||
// =====================================================
|
||||
// MQTT SETTINGS
|
||||
// CA CERT mqtt-recovery.xpto.ooguy.com (self-signed)
|
||||
// =====================================================
|
||||
|
||||
$settings = (new ConnectionSettings())
|
||||
->setUsername($recoveryUser)
|
||||
->setPassword($recoveryPass)
|
||||
->setUseTls(true)
|
||||
->setTlsSelfSignedAllowed(true)
|
||||
->setKeepAliveInterval(30)
|
||||
->setConnectTimeout(5)
|
||||
->setSocketTimeout(1)
|
||||
->setReconnectAutomatically(true);
|
||||
$recoveryXptoCaCert = <<<'EOT'
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDuTCCAqGgAwIBAgIUass5a34ekbqg0iaygCtiGRiSFHIwDQYJKoZIhvcNAQEL
|
||||
BQAwbDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDElMCMGA1UEAwwcbXF0dC1yZWNvdmVy
|
||||
eS54cHRvLm9vZ3V5LmNvbTAeFw0yNjA1MjYxMjM1NDFaFw0zNjA1MjMxMjM1NDFa
|
||||
MGwxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ
|
||||
bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxJTAjBgNVBAMMHG1xdHQtcmVjb3Zlcnku
|
||||
eHB0by5vb2d1eS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDe
|
||||
2uCWyHP/xng8DlL4SgqlXq9YbZQw/XWIoNDcpCPAirSKW/d6RNoIa4/UoUtOVfUq
|
||||
vN7JgAi+Ovz86q+aagYdVgtU/pIsj/U3/uwZ8Ga2g/AWCD2WZawE/SfpIoape6PM
|
||||
EnvSjFpyTZCLcOh6xUIdqH2/RcI7WJvgBBFkocZuP12zJD4jAIZC41+LpjUQQ6rM
|
||||
DANhmUYD2fVXdQ8f9hWuKH0inal+2eHtY6n4oL3RZdaSQNQz0j5QKeVfMuAKrMNu
|
||||
JeUEYku7Exvr6slaeoEVqlj7UwfwCPNernOvleB5y9dMKDNbXeHAFBdrPMQrgAK7
|
||||
KZvNq/5caqpzdxZfskJFAgMBAAGjUzBRMB0GA1UdDgQWBBThMl6pM+mkvCeVFwkX
|
||||
NTTnE0R2jDAfBgNVHSMEGDAWgBThMl6pM+mkvCeVFwkXNTTnE0R2jDAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDDSbFkl1BIotX6dREzyYh0H4dF
|
||||
NTU3zAayZnd4k33oIgpVnpoVd9PBwzRiRc8PuTiQv6iF2vsmJY/xghNp7UlgHo4k
|
||||
UEuF15F24fCj9Xqpbm1EgDGcDzwOsS+RD91bIXjt5U4m8S1meoJ9+xxK6LanUwp8
|
||||
FSgxiw+NGcaDMH4Dc6U6Qvxo3ycIkaJQ+SuK72RW0naC9oMp+Yq0otUjDpUkkmP9
|
||||
fNex9ArIrKPhWqWBRnImVStfY/1nzg9Eh7bS6k+qpVIEfvKVLdt2WfWDDtazaJpq
|
||||
eJSAR5CXLy3G8OKZpcSe0YXb2NkDX/R88B7z9uGD6Zz/lHE9Zot+9JS6tcvZ
|
||||
-----END CERTIFICATE-----
|
||||
EOT;
|
||||
|
||||
$recoveryXptoCaFile = '/etc/mosquitto/certs/recovery-xpto-ca.pem';
|
||||
|
||||
if (
|
||||
!file_exists($recoveryXptoCaFile) ||
|
||||
trim((string) file_get_contents($recoveryXptoCaFile)) !== trim($recoveryXptoCaCert)
|
||||
) {
|
||||
file_put_contents($recoveryXptoCaFile, $recoveryXptoCaCert);
|
||||
logmsg("CA cert escrito: $recoveryXptoCaFile");
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// MQTT CLIENT
|
||||
// BROKER PAIRS
|
||||
// =====================================================
|
||||
//
|
||||
// Cada par agrupa:
|
||||
// recvHost/Port/User/Pass → recovery broker (onde o ESP se liga quando o principal cai)
|
||||
// recvCaFile → CA para verificar o cert TLS do recovery broker
|
||||
// null = usar CA do sistema (Let's Encrypt)
|
||||
// selfSigned → true = aceitar certs self-signed (TlsSelfSignedAllowed)
|
||||
// mainHost/Port → main broker (destino do SWITCH_PRIMARY)
|
||||
// certPath → cert do main broker guardado localmente (enviado ao ESP)
|
||||
// certUrl → URL para refrescar o cert do main broker
|
||||
//
|
||||
// =====================================================
|
||||
|
||||
$mqtt = new MqttClient(
|
||||
$recoveryHost,
|
||||
$recoveryPort,
|
||||
'listener-recovery-' . getmypid()
|
||||
);
|
||||
$brokerPairs = [
|
||||
|
||||
// ── Remoto — Let's Encrypt ─────────────────────
|
||||
[
|
||||
'label' => 'remote',
|
||||
'recvHost' => 'mqtt-recovery.xupas.mywire.org',
|
||||
'recvPort' => 8884,
|
||||
'recvUser' => 'recovery',
|
||||
'recvPass' => 'recovery123',
|
||||
'recvCaFile' => null, // Let's Encrypt — CA do sistema
|
||||
'selfSigned' => true, // mantém comportamento original
|
||||
'mainHost' => 'mqtt.xupas.mywire.org',
|
||||
'mainPort' => 8883,
|
||||
'certPath' => '/etc/mosquitto/certs/device-ca.pem',
|
||||
'certUrl' => 'https://mqtt.xupas.mywire.org/cert.pem',
|
||||
],
|
||||
|
||||
// ── Local — self-signed ────────────────────────
|
||||
[
|
||||
'label' => 'local-selfsigned',
|
||||
'recvHost' => 'mqtt-recovery.xpto.ooguy.com',
|
||||
'recvPort' => 8884,
|
||||
'recvUser' => 'xupa',
|
||||
'recvPass' => 'xupa',
|
||||
'recvCaFile' => $recoveryXptoCaFile,
|
||||
'selfSigned' => true,
|
||||
'mainHost' => 'mqtt.xpto.ooguy.com',
|
||||
'mainPort' => 8883,
|
||||
'certPath' => '/etc/mosquitto/certs/device-ca.pem',
|
||||
'certUrl' => 'https://mqtt.xpto.ooguy.com/cert.pem',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
// =====================================================
|
||||
// MAIN BROKER CHECK
|
||||
@ -206,18 +253,41 @@ function send_cert_chunks(
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// SUBSCRIPTIONS
|
||||
// MAKE SETTINGS
|
||||
// =====================================================
|
||||
|
||||
$subscribe = function () use (
|
||||
$mqtt,
|
||||
$db,
|
||||
$certPath,
|
||||
$certUrl,
|
||||
$mainHost,
|
||||
$mainPort,
|
||||
$authSecret
|
||||
) {
|
||||
function make_settings(array $pair): ConnectionSettings
|
||||
{
|
||||
$s = (new ConnectionSettings())
|
||||
->setUsername($pair['recvUser'])
|
||||
->setPassword($pair['recvPass'])
|
||||
->setUseTls(true)
|
||||
->setTlsSelfSignedAllowed($pair['selfSigned'])
|
||||
->setKeepAliveInterval(30)
|
||||
->setConnectTimeout(5)
|
||||
->setSocketTimeout(1)
|
||||
->setReconnectAutomatically(true);
|
||||
|
||||
if ($pair['recvCaFile'] !== null) {
|
||||
$s = $s->setTlsCertificateAuthorityFile($pair['recvCaFile']);
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// SUBSCRIBE BROKER
|
||||
// =====================================================
|
||||
|
||||
function subscribe_broker(
|
||||
MqttClient $mqtt,
|
||||
PDO $db,
|
||||
array $pair,
|
||||
string $authSecret
|
||||
): void {
|
||||
|
||||
$label = $pair['label'];
|
||||
$certPath = $pair['certPath'];
|
||||
|
||||
// =================================================
|
||||
// AUTH
|
||||
@ -226,7 +296,7 @@ $subscribe = function () use (
|
||||
$mqtt->subscribe(
|
||||
'esp/+/auth',
|
||||
function (string $topic, string $msg)
|
||||
use ($db, $mqtt, $authSecret)
|
||||
use ($db, $mqtt, $authSecret, $label)
|
||||
{
|
||||
|
||||
$uid = explode('/', $topic)[1] ?? null;
|
||||
@ -235,17 +305,12 @@ $subscribe = function () use (
|
||||
return;
|
||||
}
|
||||
|
||||
logmsg("AUTH RX $uid => $msg");
|
||||
logmsg("[$label] AUTH RX $uid => $msg");
|
||||
|
||||
$json = json_decode($msg, true);
|
||||
|
||||
if (!$json) {
|
||||
|
||||
logmsg(
|
||||
"JSON AUTH inválido",
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
logmsg("[$label] JSON AUTH inválido", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -255,41 +320,62 @@ $subscribe = function () use (
|
||||
|
||||
if (($json['cmd'] ?? '') === 'HELLO') {
|
||||
|
||||
$nonce =
|
||||
strtoupper(
|
||||
bin2hex(random_bytes(16))
|
||||
);
|
||||
|
||||
// ─── Idempotência: evitar AUTH_CHALLENGE duplicado ─
|
||||
// Quando o listener reinicia, o broker pode reentrogar
|
||||
// mensagens QoS=1 não ACKadas pela instância anterior.
|
||||
// Se já existe um nonce recente (< 30 s) reutilizamo-lo
|
||||
// em vez de gerar um novo e sobrescrever o DB.
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO contadores
|
||||
(uid, auth_nonce, last_seen, online, blocked)
|
||||
VALUES
|
||||
(:uid, :nonce, NOW(), 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
auth_nonce=:nonce,
|
||||
last_seen=NOW(),
|
||||
online=1
|
||||
SELECT auth_nonce, auth_nonce_at
|
||||
FROM contadores
|
||||
WHERE uid=:uid
|
||||
LIMIT 1
|
||||
");
|
||||
/* novos ESPs entram com blocked=1 (pendente aprovação)
|
||||
ESPs existentes mantêm o blocked actual */
|
||||
$stmt->execute(['uid' => $uid]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt->execute([
|
||||
'uid' => $uid,
|
||||
'nonce' => $nonce
|
||||
]);
|
||||
$existingNonce = $existing['auth_nonce'] ?? null;
|
||||
$nonceAge = $existing['auth_nonce_at']
|
||||
? (time() - strtotime($existing['auth_nonce_at']))
|
||||
: PHP_INT_MAX;
|
||||
|
||||
if ($existingNonce && $nonceAge < 30) {
|
||||
// Nonce recente — reutilizar sem tocar no DB
|
||||
$nonce = $existingNonce;
|
||||
logmsg("[$label] AUTH_CHALLENGE reutilizado para $uid (age {$nonceAge}s)");
|
||||
} else {
|
||||
$nonce = strtoupper(bin2hex(random_bytes(16)));
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO contadores
|
||||
(uid, auth_nonce, auth_nonce_at, last_seen, online, blocked)
|
||||
VALUES
|
||||
(:uid, :nonce, NOW(), NOW(), 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
auth_nonce=:nonce,
|
||||
auth_nonce_at=NOW(),
|
||||
last_seen=NOW(),
|
||||
online=1
|
||||
");
|
||||
/* novos ESPs entram com blocked=1 (pendente aprovação)
|
||||
ESPs existentes mantêm o blocked actual */
|
||||
|
||||
$stmt->execute([
|
||||
'uid' => $uid,
|
||||
'nonce' => $nonce,
|
||||
]);
|
||||
}
|
||||
|
||||
$mqtt->publish(
|
||||
"esp/$uid/cmd",
|
||||
json_encode([
|
||||
'cmd' => 'AUTH_CHALLENGE',
|
||||
'nonce' => $nonce
|
||||
'cmd' => 'AUTH_CHALLENGE',
|
||||
'nonce' => $nonce,
|
||||
]),
|
||||
1
|
||||
);
|
||||
|
||||
logmsg(
|
||||
"AUTH_CHALLENGE enviado para $uid"
|
||||
);
|
||||
logmsg("[$label] AUTH_CHALLENGE enviado para $uid");
|
||||
|
||||
return;
|
||||
}
|
||||
@ -306,12 +392,7 @@ $subscribe = function () use (
|
||||
);
|
||||
|
||||
if (!$hash) {
|
||||
|
||||
logmsg(
|
||||
"AUTH sem hash",
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
logmsg("[$label] AUTH sem hash", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -326,29 +407,17 @@ $subscribe = function () use (
|
||||
'uid' => $uid
|
||||
]);
|
||||
|
||||
$row =
|
||||
$stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
|
||||
logmsg(
|
||||
"UID desconhecido: $uid",
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
logmsg("[$label] UID desconhecido: $uid", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce =
|
||||
$row['auth_nonce'] ?? '';
|
||||
$nonce = $row['auth_nonce'] ?? '';
|
||||
|
||||
if (!$nonce) {
|
||||
|
||||
logmsg(
|
||||
"Nonce vazio para $uid",
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
logmsg("[$label] Nonce vazio para $uid", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -366,7 +435,10 @@ $subscribe = function () use (
|
||||
// verificar se está aprovado
|
||||
if ((int)($row['blocked'] ?? 1) === 1) {
|
||||
|
||||
logmsg("AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação", LOG_ERROR);
|
||||
logmsg(
|
||||
"[$label] AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação",
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
$mqtt->publish(
|
||||
"esp/$uid/cmd",
|
||||
@ -377,12 +449,13 @@ $subscribe = function () use (
|
||||
return;
|
||||
}
|
||||
|
||||
logmsg("AUTH OK $uid");
|
||||
logmsg("[$label] AUTH OK $uid");
|
||||
|
||||
$stmt = $db->prepare("
|
||||
UPDATE contadores
|
||||
SET
|
||||
auth_nonce=NULL,
|
||||
auth_nonce_at=NULL,
|
||||
auth_ok=1,
|
||||
online=1,
|
||||
last_seen=NOW()
|
||||
@ -403,18 +476,10 @@ $subscribe = function () use (
|
||||
|
||||
} else {
|
||||
|
||||
logmsg(
|
||||
"AUTH FAIL $uid",
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
$mqtt->publish(
|
||||
"esp/$uid/cmd",
|
||||
json_encode([
|
||||
'cmd' => 'AUTH_FAIL'
|
||||
]),
|
||||
1
|
||||
);
|
||||
// Hash nao bate: quase sempre e cross-talk - o device respondeu
|
||||
// ao nonce do OUTRO listener (VPS). NAO mandar AUTH_FAIL (causava
|
||||
// boot-loop). So o listener cujo nonce bate manda AUTH_OK.
|
||||
logmsg("[$label] AUTH hash mismatch $uid - ignorado (cross-talk)", LOG_ERROR);
|
||||
}
|
||||
|
||||
return;
|
||||
@ -433,9 +498,7 @@ $subscribe = function () use (
|
||||
string $msg
|
||||
) use (
|
||||
$db,
|
||||
$mqtt,
|
||||
$mainHost,
|
||||
$mainPort
|
||||
$label
|
||||
) {
|
||||
|
||||
$uid = explode('/', $topic)[1] ?? null;
|
||||
@ -474,41 +537,7 @@ $subscribe = function () use (
|
||||
'uid' => $uid
|
||||
]);
|
||||
|
||||
logmsg(
|
||||
"RECOVERY STATUS $uid => $msg"
|
||||
);
|
||||
|
||||
static $lastCheck = 0;
|
||||
|
||||
if (time() - $lastCheck >= 15) {
|
||||
|
||||
$lastCheck = time();
|
||||
|
||||
logmsg("CHECK broker");
|
||||
|
||||
if (
|
||||
main_broker_online(
|
||||
$mainHost,
|
||||
$mainPort
|
||||
)
|
||||
) {
|
||||
|
||||
logmsg(
|
||||
"MAIN broker online"
|
||||
);
|
||||
|
||||
send_switch_primary(
|
||||
$mqtt,
|
||||
$db
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
logmsg(
|
||||
"MAIN broker offline"
|
||||
);
|
||||
}
|
||||
}
|
||||
logmsg("[$label] RECOVERY STATUS $uid => $msg");
|
||||
|
||||
},
|
||||
0
|
||||
@ -523,18 +552,15 @@ $subscribe = function () use (
|
||||
function (
|
||||
string $topic,
|
||||
string $msg
|
||||
) {
|
||||
) use ($label) {
|
||||
|
||||
$uid =
|
||||
explode('/', $topic)[1] ?? null;
|
||||
$uid = explode('/', $topic)[1] ?? null;
|
||||
|
||||
if (!$uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
logmsg(
|
||||
"RECOVERY EVENT $uid => $msg"
|
||||
);
|
||||
logmsg("[$label] RECOVERY EVENT $uid => $msg");
|
||||
|
||||
},
|
||||
0
|
||||
@ -552,100 +578,165 @@ $subscribe = function () use (
|
||||
) use (
|
||||
$mqtt,
|
||||
$certPath,
|
||||
$certUrl
|
||||
$label
|
||||
) {
|
||||
|
||||
$uid =
|
||||
explode('/', $topic)[1] ?? null;
|
||||
$uid = explode('/', $topic)[1] ?? null;
|
||||
|
||||
if (!$uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
logmsg("CERT_REQUEST de $uid → chunks");
|
||||
logmsg("[$label] CERT_REQUEST de $uid → chunks");
|
||||
|
||||
send_cert_chunks($mqtt, $uid, $certPath);
|
||||
|
||||
},
|
||||
1
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// MAIN LOOP
|
||||
// =====================================================
|
||||
|
||||
$clients = []; // label => MqttClient
|
||||
$nextCheck = []; // label => timestamp da próxima verificação do main broker
|
||||
$lastConnectAttempt = []; // label => timestamp da última tentativa de ligação
|
||||
|
||||
while (true) {
|
||||
|
||||
try {
|
||||
// ── connect / reconnect ────────────────────────
|
||||
|
||||
logmsg(
|
||||
"Ligar recovery $recoveryHost:$recoveryPort"
|
||||
);
|
||||
foreach ($brokerPairs as $pair) {
|
||||
|
||||
$mqtt->connect($settings, false);
|
||||
|
||||
$subscribe();
|
||||
|
||||
logmsg(
|
||||
"RECOVERY ligado e subscrito"
|
||||
);
|
||||
|
||||
$nextMainCheck = time() + 10;
|
||||
$loopStartedAt = microtime(true);
|
||||
|
||||
while ($mqtt->isConnected()) {
|
||||
|
||||
$mqtt->loopOnce($loopStartedAt, true, 100000);
|
||||
|
||||
if (time() >= $nextMainCheck) {
|
||||
|
||||
$nextMainCheck = time() + 15;
|
||||
|
||||
logmsg(
|
||||
"LOOP recovery",
|
||||
LOG_DEBUG
|
||||
);
|
||||
|
||||
if (
|
||||
main_broker_online(
|
||||
$mainHost,
|
||||
$mainPort
|
||||
)
|
||||
) {
|
||||
|
||||
logmsg(
|
||||
"MAIN broker online"
|
||||
);
|
||||
|
||||
logmsg("Mandar switch_primary");
|
||||
|
||||
send_switch_primary(
|
||||
$mqtt,
|
||||
$db
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
logmsg(
|
||||
"MAIN broker ainda offline",
|
||||
LOG_DEBUG
|
||||
);
|
||||
}
|
||||
}
|
||||
$label = $pair['label'];
|
||||
|
||||
if (
|
||||
isset($clients[$label]) &&
|
||||
$clients[$label]->isConnected()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mqtt->disconnect();
|
||||
// throttle: não tentar mais do que 1x / 5 s por broker
|
||||
if (time() < ($lastConnectAttempt[$label] ?? 0) + 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$lastConnectAttempt[$label] = time();
|
||||
|
||||
logmsg(
|
||||
"ERRO recovery: " .
|
||||
$e->getMessage(),
|
||||
LOG_ERROR
|
||||
);
|
||||
try {
|
||||
|
||||
sleep(5);
|
||||
logmsg(
|
||||
"Ligar recovery [{$label}] " .
|
||||
"{$pair['recvHost']}:{$pair['recvPort']}"
|
||||
);
|
||||
|
||||
$clients[$label] = new MqttClient(
|
||||
$pair['recvHost'],
|
||||
$pair['recvPort'],
|
||||
'listener-recovery-' . $label . '-' . getmypid()
|
||||
);
|
||||
|
||||
$clients[$label]->connect(
|
||||
make_settings($pair),
|
||||
false
|
||||
);
|
||||
|
||||
subscribe_broker(
|
||||
$clients[$label],
|
||||
$db,
|
||||
$pair,
|
||||
$authSecret
|
||||
);
|
||||
|
||||
// primeira verificação do main broker após 10 s
|
||||
$nextCheck[$label] = time() + 10;
|
||||
|
||||
logmsg("[{$label}] ligado e subscrito");
|
||||
|
||||
} catch (Throwable $e) {
|
||||
|
||||
logmsg(
|
||||
"[{$label}] ERRO connect: " . $e->getMessage(),
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
unset($clients[$label]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($clients)) {
|
||||
sleep(5);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── drive all clients (50 ms cada) ────────────
|
||||
|
||||
$loopStartedAt = microtime(true);
|
||||
|
||||
foreach ($clients as $label => $mqtt) {
|
||||
|
||||
try {
|
||||
|
||||
$mqtt->loopOnce($loopStartedAt, true, 50000);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
|
||||
logmsg(
|
||||
"[{$label}] ERRO loop: " . $e->getMessage(),
|
||||
LOG_ERROR
|
||||
);
|
||||
|
||||
try {
|
||||
$mqtt->disconnect();
|
||||
} catch (Throwable $_) {}
|
||||
|
||||
unset($clients[$label]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── periodic main broker check per pair ────────
|
||||
|
||||
$now = time();
|
||||
|
||||
foreach ($brokerPairs as $pair) {
|
||||
|
||||
$label = $pair['label'];
|
||||
|
||||
if (
|
||||
!isset($clients[$label]) ||
|
||||
!$clients[$label]->isConnected()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($now < ($nextCheck[$label] ?? 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nextCheck[$label] = $now + 15;
|
||||
|
||||
logmsg("[{$label}] CHECK main broker", LOG_DEBUG);
|
||||
|
||||
if (
|
||||
main_broker_online(
|
||||
$pair['mainHost'],
|
||||
$pair['mainPort']
|
||||
)
|
||||
) {
|
||||
|
||||
logmsg("[{$label}] MAIN broker online → SWITCH_PRIMARY");
|
||||
|
||||
send_switch_primary($clients[$label], $db);
|
||||
|
||||
} else {
|
||||
|
||||
logmsg(
|
||||
"[{$label}] MAIN broker ainda offline",
|
||||
LOG_DEBUG
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
api/menu.php
164
api/menu.php
@ -1,163 +1,3 @@
|
||||
<?php
|
||||
require_once "protecao.php";
|
||||
|
||||
/* =========================
|
||||
CONTROLO DA VPN
|
||||
========================= */
|
||||
|
||||
$outputVPN = null;
|
||||
|
||||
if (isset($_GET['vpn'])) {
|
||||
$allowed = ['md','al','nl','de','ch','status'];
|
||||
$cmd = $_GET['vpn'];
|
||||
|
||||
if (in_array($cmd, $allowed)) {
|
||||
// IP do LXC NordVPN (104)
|
||||
$vpnHost = "192.168.10.104";
|
||||
$vpnUser = "root";
|
||||
|
||||
$sshCmd = "/usr/bin/ssh -i /var/www/.ssh/nordvpn "
|
||||
. "-o BatchMode=yes -o StrictHostKeyChecking=no "
|
||||
. "{$vpnUser}@{$vpnHost} "
|
||||
. escapeshellarg($cmd)
|
||||
. " 2>&1";
|
||||
|
||||
$outputVPN = shell_exec($sshCmd);
|
||||
} else {
|
||||
$outputVPN = "❌ Comando VPN inválido.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Menu Principal</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #0f0f0f;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
h2, h4 {
|
||||
color: #00ffc8;
|
||||
text-shadow: 0 0 6px #00ffc8;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-width: 2px;
|
||||
font-weight: 600;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #000;
|
||||
color: #00ff99;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 50px;
|
||||
font-size: 0.85rem;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="p-4">
|
||||
<div class="container text-center">
|
||||
|
||||
<h2 class="mb-4">
|
||||
Bem-vindo, <?= htmlspecialchars($_SESSION['username']) ?> 😎
|
||||
</h2>
|
||||
|
||||
<!-- MENU PRINCIPAL -->
|
||||
<div class="row g-2 mb-5">
|
||||
<div class="col-12">
|
||||
<a href="../console.php" class="btn btn-outline-light w-100">📊 Estado dos Dispositivos</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="../devices.php" class="btn btn-outline-warning w-100">🔌 Gerir Dispositivos</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="../comandos.php" class="btn btn-outline-light w-100">🛠️ Enviar Comando</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="../historico.php" class="btn btn-outline-light w-100">📜 Ver Histórico</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="config.php" class="btn btn-outline-info w-100">⚙️ Configurações</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="https://web.xupas.mywire.org/matrix_admin.php" target="_blank"
|
||||
class="btn btn-outline-success w-100">
|
||||
💬 Matrix Admin (Web)
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="https://admmatrix.xupas.mywire.org" target="_blank"
|
||||
class="btn btn-outline-warning w-100">
|
||||
🧠 Synapse Admin (Servidor)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTROLO DA VPN -->
|
||||
<hr class="my-4">
|
||||
<h4 class="mb-3">🌍 Controlo da VPN</h4>
|
||||
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<a href="?vpn=md" class="btn btn-outline-success w-100">🇲🇩 Moldova (YouTube)</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<a href="?vpn=al" class="btn btn-outline-success w-100">🇦🇱 Albania (Ad-free)</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<a href="?vpn=nl" class="btn btn-outline-info w-100">🇳🇱 Netherlands (Torrent)</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<a href="?vpn=de" class="btn btn-outline-info w-100">🇩🇪 Germany (Speed)</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="?vpn=status" class="btn btn-outline-light w-100">📡 Ver Estado VPN</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($outputVPN): ?>
|
||||
<div class="mb-4">
|
||||
<pre><?= htmlspecialchars($outputVPN) ?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- LOGOUT -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="logout.php" class="btn btn-outline-danger w-100">🚪 Terminar Sessão</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center mt-5">
|
||||
<p>© <?= date('Y') ?> Xupas Systems | Todos os direitos reservados</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
header("Location: /vpn.php");
|
||||
exit;
|
||||
|
||||
22
api/mqtt.php
22
api/mqtt.php
@ -1,11 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function mqtt_publish(string $topic, string $msg): void {
|
||||
$cmd = sprintf(
|
||||
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
|
||||
escapeshellarg($topic),
|
||||
escapeshellarg($msg)
|
||||
);
|
||||
shell_exec($cmd . " > /dev/null 2>&1");
|
||||
}
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function mqtt_publish(string $topic, string $msg): void {
|
||||
$cmd = sprintf(
|
||||
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
|
||||
escapeshellarg($topic),
|
||||
escapeshellarg($msg)
|
||||
);
|
||||
shell_exec($cmd . " > /dev/null 2>&1");
|
||||
}
|
||||
|
||||
@ -1,44 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Reset de parciais com acumulação em totais (por máquina):
|
||||
* total += parcial; parcial = 0
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED & ~E_NOTICE & ~E_USER_NOTICE);
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
try {
|
||||
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
// garantir colunas (ignora se já existirem)
|
||||
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
|
||||
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
|
||||
|
||||
$uid = $_POST['uid'] ?? '';
|
||||
if ($uid === '') {
|
||||
header("Location: /console.php?ok=err");
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
UPDATE contadores
|
||||
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
|
||||
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
|
||||
entradas = 0,
|
||||
saidas = 0
|
||||
WHERE uid = :uid
|
||||
");
|
||||
$stmt->execute(['uid' => $uid]);
|
||||
|
||||
header("Location: /console.php?ok=line");
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
header("Location: /console.php?ok=err");
|
||||
exit;
|
||||
}
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Reset de parciais com acumulação em totais (por máquina):
|
||||
* total += parcial; parcial = 0
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED & ~E_NOTICE & ~E_USER_NOTICE);
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
try {
|
||||
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
// garantir colunas (ignora se já existirem)
|
||||
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
|
||||
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
|
||||
|
||||
$uid = $_POST['uid'] ?? '';
|
||||
if ($uid === '') {
|
||||
header("Location: /console.php?ok=err");
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
UPDATE contadores
|
||||
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
|
||||
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
|
||||
entradas = 0,
|
||||
saidas = 0
|
||||
WHERE uid = :uid
|
||||
");
|
||||
$stmt->execute(['uid' => $uid]);
|
||||
|
||||
header("Location: /console.php?ok=line");
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
header("Location: /console.php?ok=err");
|
||||
exit;
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<?php
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
|
||||
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
|
||||
$stmt->execute([$newPassword]);
|
||||
|
||||
echo "✅ Password de admin alterada para admin123";
|
||||
<?php
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
|
||||
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
|
||||
$stmt->execute([$newPassword]);
|
||||
|
||||
echo "✅ Password de admin alterada para admin123";
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require __DIR__ . '/db.php'; // ligação PDO
|
||||
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error'=>'invalid_request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uid = $data['uid'];
|
||||
$cmd = $data['cmd'];
|
||||
$value = $data['value'] ?? null;
|
||||
|
||||
// ----------------------- MQTT -----------------------
|
||||
$topic = "esp/$uid/cmd";
|
||||
|
||||
$msg = ['cmd' => $cmd];
|
||||
if ($value !== null) $msg['value'] = $value;
|
||||
|
||||
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
|
||||
|
||||
// ----------------------- BD -------------------------
|
||||
if ($cmd === 'BLOCK') {
|
||||
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
|
||||
$stmt->execute([intval($value), $uid]);
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>1]);
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require __DIR__ . '/db.php'; // ligação PDO
|
||||
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error'=>'invalid_request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$uid = $data['uid'];
|
||||
$cmd = $data['cmd'];
|
||||
$value = $data['value'] ?? null;
|
||||
|
||||
// ----------------------- MQTT -----------------------
|
||||
$topic = "esp/$uid/cmd";
|
||||
|
||||
$msg = ['cmd' => $cmd];
|
||||
if ($value !== null) $msg['value'] = $value;
|
||||
|
||||
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
|
||||
|
||||
// ----------------------- BD -------------------------
|
||||
if ($cmd === 'BLOCK') {
|
||||
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
|
||||
$stmt->execute([intval($value), $uid]);
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>1]);
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
<?php
|
||||
set_time_limit(30);
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
use PhpMqtt\Client\ConnectionSettings;
|
||||
|
||||
$data = [];
|
||||
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
|
||||
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
|
||||
$mqtt->connect($settings, true);
|
||||
|
||||
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
|
||||
$data[] = ['topic'=>$topic, 'message'=>$message];
|
||||
if (count($data) >= 5) {
|
||||
$mqtt->interrupt(); // para o loop depois de 5 msg
|
||||
}
|
||||
}, 0);
|
||||
|
||||
$mqtt->loop(true);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data, JSON_PRETTY_PRINT);
|
||||
<?php
|
||||
set_time_limit(30);
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
use PhpMqtt\Client\ConnectionSettings;
|
||||
|
||||
$data = [];
|
||||
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
|
||||
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
|
||||
$mqtt->connect($settings, true);
|
||||
|
||||
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
|
||||
$data[] = ['topic'=>$topic, 'message'=>$message];
|
||||
if (count($data) >= 5) {
|
||||
$mqtt->interrupt(); // para o loop depois de 5 msg
|
||||
}
|
||||
}, 0);
|
||||
|
||||
$mqtt->loop(true);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data, JSON_PRETTY_PRINT);
|
||||
|
||||
@ -33,7 +33,7 @@ try {
|
||||
$_SESSION['username'] = $user['username'];
|
||||
|
||||
// Redireciona para o menu principal
|
||||
header("Location: /api/menu.php");
|
||||
header("Location: /painel.php");
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
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:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- octocat
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Deprecated
|
||||
labels:
|
||||
- deprecated
|
||||
- title: Removed
|
||||
labels:
|
||||
- removed
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bug
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Changed
|
||||
labels:
|
||||
- "*"
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- octocat
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Deprecated
|
||||
labels:
|
||||
- deprecated
|
||||
- title: Removed
|
||||
labels:
|
||||
- removed
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bug
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Changed
|
||||
labels:
|
||||
- "*"
|
||||
|
||||
110
comandosbck.php
110
comandosbck.php
@ -1,55 +1,55 @@
|
||||
<?php
|
||||
// ---------------- DB ----------------
|
||||
$dbHost = "localhost";
|
||||
$dbName = "cagalhao";
|
||||
$dbUser = "master";
|
||||
$dbPass = "master";
|
||||
|
||||
try {
|
||||
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die("Erro DB: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
|
||||
$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Histórico de Comandos</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #111; color: #eee; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
|
||||
th, td { border: 1px solid #444; padding: 6px 10px; text-align: center; }
|
||||
th { background: #222; }
|
||||
tr:nth-child(even) { background: #1b1b1b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Histórico de Comandos</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>UID</th>
|
||||
<th>CMD</th>
|
||||
<th>Params</th>
|
||||
<th>Status</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
<?php foreach ($comandos as $cmd): ?>
|
||||
<tr>
|
||||
<td><?= $cmd['id'] ?></td>
|
||||
<td><?= htmlspecialchars($cmd['uid']) ?></td>
|
||||
<td><?= htmlspecialchars($cmd['cmd']) ?></td>
|
||||
<td><pre><?= htmlspecialchars($cmd['params']) ?></pre></td>
|
||||
<td><?= $cmd['status'] ?></td>
|
||||
<td><?= $cmd['created_at'] ?? '' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<p><a href="/api/menu.php">⬅️ Voltar</a></p>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
// ---------------- DB ----------------
|
||||
$dbHost = "localhost";
|
||||
$dbName = "cagalhao";
|
||||
$dbUser = "master";
|
||||
$dbPass = "master";
|
||||
|
||||
try {
|
||||
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die("Erro DB: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
|
||||
$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Histórico de Comandos</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #111; color: #eee; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
|
||||
th, td { border: 1px solid #444; padding: 6px 10px; text-align: center; }
|
||||
th { background: #222; }
|
||||
tr:nth-child(even) { background: #1b1b1b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Histórico de Comandos</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>UID</th>
|
||||
<th>CMD</th>
|
||||
<th>Params</th>
|
||||
<th>Status</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
<?php foreach ($comandos as $cmd): ?>
|
||||
<tr>
|
||||
<td><?= $cmd['id'] ?></td>
|
||||
<td><?= htmlspecialchars($cmd['uid']) ?></td>
|
||||
<td><?= htmlspecialchars($cmd['cmd']) ?></td>
|
||||
<td><pre><?= htmlspecialchars($cmd['params']) ?></pre></td>
|
||||
<td><?= $cmd['status'] ?></td>
|
||||
<td><?= $cmd['created_at'] ?? '' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<p><a href="/api/menu.php">⬅️ Voltar</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22
config.php
22
config.php
@ -1,11 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'host' => 'mqtt.xupas.mywire.org',
|
||||
'port' => 8883,
|
||||
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
|
||||
'username' => 'xupa',
|
||||
'password' => 'xupa',
|
||||
'clean' => true,
|
||||
'keepalive'=> 60,
|
||||
'timeout' => 5,
|
||||
];
|
||||
<?php
|
||||
return [
|
||||
'host' => 'mqtt.xupas.mywire.org',
|
||||
'port' => 8883,
|
||||
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
|
||||
'username' => 'xupa',
|
||||
'password' => 'xupa',
|
||||
'clean' => true,
|
||||
'keepalive'=> 60,
|
||||
'timeout' => 5,
|
||||
];
|
||||
|
||||
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("
|
||||
SELECT uid, note, last_seen, online, blocked, auth_ok, current_broker,
|
||||
COALESCE(brightness, 128) AS brightness,
|
||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||
FROM contadores
|
||||
WHERE blocked=0 OR auth_ok=1
|
||||
@ -208,6 +209,7 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
|
||||
<th>ESTADO</th>
|
||||
<th>BROKER</th>
|
||||
<th>ÚLTIMO</th>
|
||||
<th>BRILHO</th>
|
||||
<th>AÇÕES</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -231,12 +233,27 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
|
||||
? '<span class="badge bg-warning text-dark">RECOVERY</span>'
|
||||
: '<span class="badge bg-primary">PRIMARY</span>';
|
||||
?>
|
||||
<?php $bri = (int)($r['brightness'] ?? 128); ?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= htmlspecialchars($r['uid']) ?></td>
|
||||
<td><?= htmlspecialchars($r['note'] ?? '') ?></td>
|
||||
<td><?= $state ?></td>
|
||||
<td><?= $broker ?></td>
|
||||
<td><?= $lastSeen ?></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="range" class="form-range" min="0" max="255"
|
||||
value="<?= $bri ?>"
|
||||
id="bri_<?= htmlspecialchars($r['uid']) ?>"
|
||||
style="width:110px"
|
||||
oninput="setBri('<?= htmlspecialchars($r['uid']) ?>',this.value)"
|
||||
<?= $online ? '' : 'disabled' ?>>
|
||||
<span id="brival_<?= htmlspecialchars($r['uid']) ?>"
|
||||
class="text-muted" style="min-width:28px;font-size:.85em">
|
||||
<?= $bri ?>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="?toggle=<?= urlencode($r['uid']) ?>"
|
||||
@ -262,5 +279,20 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const briTimers = {};
|
||||
function setBri(uid, val) {
|
||||
document.getElementById('brival_' + uid).textContent = val;
|
||||
clearTimeout(briTimers[uid]);
|
||||
briTimers[uid] = setTimeout(() => {
|
||||
const body = new URLSearchParams({ uid, value: val });
|
||||
fetch('/api/set_brightness.php', { method: 'POST', body })
|
||||
.then(r => r.json())
|
||||
.then(d => { if (!d.ok) console.warn('brightness err', d); })
|
||||
.catch(e => console.error(e));
|
||||
}, 350);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header("Content-Type: application/json; charset=utf-8");
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$cfg = @include __DIR__ . "/config_db.php";
|
||||
if (!is_array($cfg)) {
|
||||
throw new RuntimeException("Config DB inválida ou em falta (config_db.php).");
|
||||
}
|
||||
|
||||
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", $cfg['db_host'], $cfg['db_name']);
|
||||
$pdo = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT token_plain FROM api_tokens WHERE label = ? AND revoked = 0 ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute(['matrix-admin']);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || empty($row['token_plain'])) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "Token não encontrado"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(["token" => $row["token_plain"]]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => $e->getMessage()]);
|
||||
}
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header("Content-Type: application/json; charset=utf-8");
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$cfg = @include __DIR__ . "/config_db.php";
|
||||
if (!is_array($cfg)) {
|
||||
throw new RuntimeException("Config DB inválida ou em falta (config_db.php).");
|
||||
}
|
||||
|
||||
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", $cfg['db_host'], $cfg['db_name']);
|
||||
$pdo = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT token_plain FROM api_tokens WHERE label = ? AND revoked = 0 ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute(['matrix-admin']);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || empty($row['token_plain'])) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "Token não encontrado"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(["token" => $row["token_plain"]]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => $e->getMessage()]);
|
||||
}
|
||||
|
||||
312
historico.php
312
historico.php
@ -1,156 +1,156 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/api/protecao.php";
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
// Se pediu para limpar pendentes
|
||||
if (isset($_POST['clear_pending'])) {
|
||||
$pdo->exec("DELETE FROM comandos WHERE status = 'pending'");
|
||||
$_SESSION['flash'] = "🧹 Todos os comandos pendentes foram removidos!";
|
||||
header("Location: historico.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Filtros
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['uid'])) {
|
||||
$where[] = "c.uid = ?";
|
||||
$params[] = $_GET['uid'];
|
||||
}
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = "c.status = ?";
|
||||
$params[] = $_GET['status'];
|
||||
}
|
||||
|
||||
$sql = "SELECT c.id, c.uid, c.cmd, c.status, c.queued_at, c.sent_at, c.ack_ts, c.error,
|
||||
r.payload AS resposta, r.ts AS resposta_at
|
||||
FROM comandos c
|
||||
LEFT JOIN respostas r ON r.uid = c.uid
|
||||
" . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
|
||||
ORDER BY c.queued_at DESC
|
||||
LIMIT 50";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$comandos = $stmt->fetchAll();
|
||||
|
||||
$devices = $pdo->query("SELECT DISTINCT uid FROM devices ORDER BY uid ASC")->fetchAll();
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("Erro: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Histórico de Comandos</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #121212; color: #eee; }
|
||||
.card, .table { background-color: #1e1e1e; color: #ddd; }
|
||||
.table thead { background-color: #333; }
|
||||
</style>
|
||||
<script>
|
||||
// Auto-refresh a cada 5 segundos
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-4">
|
||||
<div class="container-fluid">
|
||||
<h2 class="mb-4 text-info">📜 Histórico de Comandos</h2>
|
||||
|
||||
<?php if (!empty($_SESSION['flash'])): ?>
|
||||
<div class="alert alert-success"><?= $_SESSION['flash']; unset($_SESSION['flash']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Botão para limpar pendentes -->
|
||||
<form method="POST" class="mb-3">
|
||||
<button type="submit" name="clear_pending" class="btn btn-danger">
|
||||
🧹 Limpar Pendentes
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Filtros -->
|
||||
<form method="GET" class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<select name="uid" class="form-select">
|
||||
<option value="">-- Todos os dispositivos --</option>
|
||||
<?php foreach ($devices as $d): ?>
|
||||
<option value="<?= htmlspecialchars($d['uid']) ?>" <?= (($_GET['uid'] ?? '') === $d['uid']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($d['uid']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">-- Todos os status --</option>
|
||||
<?php foreach (['pending','sent','done','failed'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= (($_GET['status'] ?? '') === $s) ? 'selected' : '' ?>>
|
||||
<?= ucfirst($s) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-primary w-100">🔎 Filtrar</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Tabela -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Dispositivo</th>
|
||||
<th>Comando</th>
|
||||
<th>Status</th>
|
||||
<th>Enfileirado</th>
|
||||
<th>Enviado</th>
|
||||
<th>ACK</th>
|
||||
<th>Erro</th>
|
||||
<th>Resposta</th>
|
||||
<th>Resp. At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($comandos as $c): ?>
|
||||
<?php
|
||||
switch ($c['status']) {
|
||||
case 'pending': $badge = 'warning'; break;
|
||||
case 'sent': $badge = 'info'; break;
|
||||
case 'done': $badge = 'success'; break;
|
||||
case 'failed': $badge = 'danger'; break;
|
||||
default: $badge = 'secondary'; break;
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $c['id'] ?></td>
|
||||
<td><?= htmlspecialchars($c['uid']) ?></td>
|
||||
<td><?= htmlspecialchars($c['cmd']) ?></td>
|
||||
<td><span class="badge bg-<?= $badge ?>"><?= $c['status'] ?></span></td>
|
||||
<td><?= $c['queued_at'] ?></td>
|
||||
<td><?= $c['sent_at'] ?></td>
|
||||
<td><?= $c['ack_ts'] ?></td>
|
||||
<td><?= htmlspecialchars($c['error'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($c['resposta'] ?? '') ?></td>
|
||||
<td><?= $c['resposta_at'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
require_once __DIR__ . "/api/protecao.php";
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
// Se pediu para limpar pendentes
|
||||
if (isset($_POST['clear_pending'])) {
|
||||
$pdo->exec("DELETE FROM comandos WHERE status = 'pending'");
|
||||
$_SESSION['flash'] = "🧹 Todos os comandos pendentes foram removidos!";
|
||||
header("Location: historico.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Filtros
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['uid'])) {
|
||||
$where[] = "c.uid = ?";
|
||||
$params[] = $_GET['uid'];
|
||||
}
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = "c.status = ?";
|
||||
$params[] = $_GET['status'];
|
||||
}
|
||||
|
||||
$sql = "SELECT c.id, c.uid, c.cmd, c.status, c.queued_at, c.sent_at, c.ack_ts, c.error,
|
||||
r.payload AS resposta, r.ts AS resposta_at
|
||||
FROM comandos c
|
||||
LEFT JOIN respostas r ON r.uid = c.uid
|
||||
" . ($where ? "WHERE " . implode(" AND ", $where) : "") . "
|
||||
ORDER BY c.queued_at DESC
|
||||
LIMIT 50";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$comandos = $stmt->fetchAll();
|
||||
|
||||
$devices = $pdo->query("SELECT DISTINCT uid FROM devices ORDER BY uid ASC")->fetchAll();
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("Erro: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Histórico de Comandos</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #121212; color: #eee; }
|
||||
.card, .table { background-color: #1e1e1e; color: #ddd; }
|
||||
.table thead { background-color: #333; }
|
||||
</style>
|
||||
<script>
|
||||
// Auto-refresh a cada 5 segundos
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
</script>
|
||||
</head>
|
||||
<body class="p-4">
|
||||
<div class="container-fluid">
|
||||
<h2 class="mb-4 text-info">📜 Histórico de Comandos</h2>
|
||||
|
||||
<?php if (!empty($_SESSION['flash'])): ?>
|
||||
<div class="alert alert-success"><?= $_SESSION['flash']; unset($_SESSION['flash']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Botão para limpar pendentes -->
|
||||
<form method="POST" class="mb-3">
|
||||
<button type="submit" name="clear_pending" class="btn btn-danger">
|
||||
🧹 Limpar Pendentes
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Filtros -->
|
||||
<form method="GET" class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<select name="uid" class="form-select">
|
||||
<option value="">-- Todos os dispositivos --</option>
|
||||
<?php foreach ($devices as $d): ?>
|
||||
<option value="<?= htmlspecialchars($d['uid']) ?>" <?= (($_GET['uid'] ?? '') === $d['uid']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($d['uid']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">-- Todos os status --</option>
|
||||
<?php foreach (['pending','sent','done','failed'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= (($_GET['status'] ?? '') === $s) ? 'selected' : '' ?>>
|
||||
<?= ucfirst($s) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-primary w-100">🔎 Filtrar</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Tabela -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Dispositivo</th>
|
||||
<th>Comando</th>
|
||||
<th>Status</th>
|
||||
<th>Enfileirado</th>
|
||||
<th>Enviado</th>
|
||||
<th>ACK</th>
|
||||
<th>Erro</th>
|
||||
<th>Resposta</th>
|
||||
<th>Resp. At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($comandos as $c): ?>
|
||||
<?php
|
||||
switch ($c['status']) {
|
||||
case 'pending': $badge = 'warning'; break;
|
||||
case 'sent': $badge = 'info'; break;
|
||||
case 'done': $badge = 'success'; break;
|
||||
case 'failed': $badge = 'danger'; break;
|
||||
default: $badge = 'secondary'; break;
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $c['id'] ?></td>
|
||||
<td><?= htmlspecialchars($c['uid']) ?></td>
|
||||
<td><?= htmlspecialchars($c['cmd']) ?></td>
|
||||
<td><span class="badge bg-<?= $badge ?>"><?= $c['status'] ?></span></td>
|
||||
<td><?= $c['queued_at'] ?></td>
|
||||
<td><?= $c['sent_at'] ?></td>
|
||||
<td><?= $c['ack_ts'] ?></td>
|
||||
<td><?= htmlspecialchars($c['error'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($c['resposta'] ?? '') ?></td>
|
||||
<td><?= $c['resposta_at'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="api/menu.php" class="btn btn-secondary">⬅️ Voltar ao Menu</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
102
homeserver.yaml
102
homeserver.yaml
@ -1,51 +1,51 @@
|
||||
server_name: "matrix.xupas.mywire.org"
|
||||
pid_file: /data/homeserver.pid
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: psycopg2
|
||||
args:
|
||||
user: synapse
|
||||
password: synapse
|
||||
database: synapse
|
||||
host: matrix-postgres-1
|
||||
cp_min: 5
|
||||
cp_max: 10
|
||||
allow_unsafe_locale: true
|
||||
|
||||
experimental_features:
|
||||
msc3202_enabled: true
|
||||
msc3077_enabled: true
|
||||
msc3401_enabled: true
|
||||
msc3391_enabled: true
|
||||
msc3890_enabled: true
|
||||
msc4153_enabled: true
|
||||
|
||||
turn_uris:
|
||||
- "turn:192.168.10.253:5349?transport=udp"
|
||||
- "turn:192.168.10.253:5349?transport=tcp"
|
||||
turn_shared_secret: "c246729fea23718edc2bf08726f6822f6732b1bde3c2e062194fc5e6758b0cf7"
|
||||
turn_user_lifetime: 86400000
|
||||
turn_allow_guests: true
|
||||
|
||||
log_config: "/data/matrix.xupas.mywire.org.log.config"
|
||||
media_store_path: /data/media_store
|
||||
registration_shared_secret: "c#PmLj2F,GAR,xFWL.uw-Gb0TF47oKD^Nb@a~h~D@WRVBHdo*y"
|
||||
report_stats: true
|
||||
macaroon_secret_key: "5XLRSbZP&ij+Dh=SC4^hUL2SjleMkIS69=uPsS&n6D8aLqJGqA"
|
||||
form_secret: "ht7o4U6Ue@S7HBDVVJczKH*4h_gBkADg@ursU0Ye*RBIn7R8w8"
|
||||
signing_key_path: "/data/matrix.xupas.mywire.org.signing.key"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
|
||||
suppress_key_server_warning: true
|
||||
enable_registration: false
|
||||
enable_registration_without_verification: false
|
||||
|
||||
server_name: "matrix.xupas.mywire.org"
|
||||
pid_file: /data/homeserver.pid
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: psycopg2
|
||||
args:
|
||||
user: synapse
|
||||
password: synapse
|
||||
database: synapse
|
||||
host: matrix-postgres-1
|
||||
cp_min: 5
|
||||
cp_max: 10
|
||||
allow_unsafe_locale: true
|
||||
|
||||
experimental_features:
|
||||
msc3202_enabled: true
|
||||
msc3077_enabled: true
|
||||
msc3401_enabled: true
|
||||
msc3391_enabled: true
|
||||
msc3890_enabled: true
|
||||
msc4153_enabled: true
|
||||
|
||||
turn_uris:
|
||||
- "turn:192.168.10.253:5349?transport=udp"
|
||||
- "turn:192.168.10.253:5349?transport=tcp"
|
||||
turn_shared_secret: "c246729fea23718edc2bf08726f6822f6732b1bde3c2e062194fc5e6758b0cf7"
|
||||
turn_user_lifetime: 86400000
|
||||
turn_allow_guests: true
|
||||
|
||||
log_config: "/data/matrix.xupas.mywire.org.log.config"
|
||||
media_store_path: /data/media_store
|
||||
registration_shared_secret: "c#PmLj2F,GAR,xFWL.uw-Gb0TF47oKD^Nb@a~h~D@WRVBHdo*y"
|
||||
report_stats: true
|
||||
macaroon_secret_key: "5XLRSbZP&ij+Dh=SC4^hUL2SjleMkIS69=uPsS&n6D8aLqJGqA"
|
||||
form_secret: "ht7o4U6Ue@S7HBDVVJczKH*4h_gBkADg@ursU0Ye*RBIn7R8w8"
|
||||
signing_key_path: "/data/matrix.xupas.mywire.org.signing.key"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
|
||||
suppress_key_server_warning: true
|
||||
enable_registration: false
|
||||
enable_registration_without_verification: false
|
||||
|
||||
|
||||
558
matrix_admin.php
558
matrix_admin.php
@ -1,279 +1,279 @@
|
||||
<?php declare(strict_types=1);
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
// ==============================
|
||||
// ⚙️ CONFIGURAÇÃO
|
||||
// ==============================
|
||||
$MATRIX_BASE = "https://matrix.xupas.mywire.org";
|
||||
$SERVER_NAME = "matrix.xupas.mywire.org";
|
||||
$TOKEN_FILE = "/var/www/secure/matrix_token.txt";
|
||||
|
||||
$ADMIN_TOKEN = is_readable($TOKEN_FILE) ? trim(file_get_contents($TOKEN_FILE)) : '';
|
||||
|
||||
$DEBUG_LOG = "/tmp/matrix_debug.log";
|
||||
|
||||
// ==============================
|
||||
// 📝 DEBUG HELPER
|
||||
// ==============================
|
||||
function dbg($txt) {
|
||||
global $DEBUG_LOG;
|
||||
file_put_contents($DEBUG_LOG, "[".date('Y-m-d H:i:s')."] $txt\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 🔧 API CALL
|
||||
// ==============================
|
||||
function call_api(string $method, string $url, ?array $data = null, ?string $token = null): array {
|
||||
dbg("API CALL: $method $url DATA=" . json_encode($data));
|
||||
$ch = curl_init($url);
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($token) $headers[] = "Authorization: Bearer $token";
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
]);
|
||||
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
dbg("API RESP: HTTP=$code BODY=$resp");
|
||||
curl_close($ch);
|
||||
return [$code, $resp];
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 🧼 USERNAME CLEANER FIX
|
||||
// ==============================
|
||||
function full_user(string $name, string $server): string {
|
||||
dbg("RAW POST USER=[".$name."] HEX=[".bin2hex($name)."]");
|
||||
$clean = trim($name);
|
||||
$clean = preg_replace('/[\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]/u', '', $clean);
|
||||
dbg("CLEAN USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
||||
if (preg_match('/^@[a-z0-9=_\-\.\/\+]+:[a-z0-9\.\-]+$/i', $clean)) return $clean;
|
||||
if ($clean === '') return '';
|
||||
if ($clean[0] !== '@') $clean = '@' . $clean;
|
||||
if (!str_contains($clean, ':')) $clean .= ':' . $server;
|
||||
dbg("FINAL USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
||||
return $clean;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 🚦 PROCESSAMENTO DE AÇÕES
|
||||
// ==============================
|
||||
$action = $_POST['action'] ?? null;
|
||||
$msg = '';
|
||||
|
||||
if ($action && $ADMIN_TOKEN) {
|
||||
|
||||
dbg("=== ACTION: $action ===");
|
||||
dbg("POST DATA: " . json_encode($_POST));
|
||||
|
||||
// criar utilizador
|
||||
if ($action === 'add_user') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$pass = trim($_POST['pass']);
|
||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||
"password" => $pass,
|
||||
"deactivated" => false,
|
||||
"admin" => false
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200 || $code == 201) ? "✅ Criado: $user" : "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// alterar password
|
||||
if ($action === 'change_pass') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$pass = trim($_POST['pass']);
|
||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/reset_password/" . urlencode($user), [
|
||||
"new_password" => $pass
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200) ? "🔑 Senha alterada para $user" : "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// admin / unadmin
|
||||
if ($action === 'make_admin' || $action === 'revoke_admin') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$is_admin = $action === 'make_admin';
|
||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||
"admin" => $is_admin
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200)
|
||||
? ($is_admin ? "⬆️ $user agora é admin" : "⬇️ $user deixou de ser admin")
|
||||
: "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// ativar / desativar
|
||||
if ($action === 'deactivate_user' || $action === 'reactivate_user') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$active = $action === 'reactivate_user';
|
||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||
"deactivated" => !$active
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200)
|
||||
? ($active ? "♻️ $user reativado" : "🚫 $user desativado")
|
||||
: "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// apagar user
|
||||
if ($action === 'delete_user') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/deactivate/" . urlencode($user), [
|
||||
"erase" => true
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200 || $code == 204)
|
||||
? "🗑️ $user removido permanentemente"
|
||||
: "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// ✅ NOVA: apagar mensagens do user (mantendo o user)
|
||||
if ($action === 'erase_messages') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/user/$user/rooms", null, $ADMIN_TOKEN);
|
||||
$rooms = json_decode($resp, true)['rooms'] ?? [];
|
||||
$errors = [];
|
||||
foreach ($rooms as $room) {
|
||||
[$c, $r] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/purge_history/" . urlencode($room['room_id']), [
|
||||
"delete_local_events" => true,
|
||||
"reason" => "Apagadas via painel"
|
||||
], $ADMIN_TOKEN);
|
||||
if ($c != 200) $errors[] = "$room[room_id]: $r";
|
||||
}
|
||||
$msg = empty($errors) ? "🧹 Mensagens de $user apagadas" : "❌ Erros: " . implode("; ", $errors);
|
||||
}
|
||||
|
||||
// ✅ NOVA: limpar metadados gerais
|
||||
if ($action === 'clear_metadata') {
|
||||
@unlink($DEBUG_LOG);
|
||||
// aqui podes adicionar mais caminhos de logs/metadados
|
||||
$msg = "⚡ Metadados limpos!";
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 📋 LISTAGEM USERS
|
||||
// ==============================
|
||||
$users = [];
|
||||
if ($ADMIN_TOKEN) {
|
||||
$next = null;
|
||||
do {
|
||||
$url = "$MATRIX_BASE/_synapse/admin/v2/users?limit=100";
|
||||
if ($next) $url .= "&from=" . urlencode($next);
|
||||
[$code, $resp] = call_api("GET", $url, null, $ADMIN_TOKEN);
|
||||
if ($code != 200) { dbg("ERRO AO LER USERS: HTTP=$code BODY=$resp"); break; }
|
||||
$data = json_decode($resp, true);
|
||||
$chunk = $data['users'] ?? $data['results'] ?? [];
|
||||
$users = array_merge($users, $chunk);
|
||||
$next = $data['next_token'] ?? null;
|
||||
} while ($next);
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Matrix Admin - Xupas</title>
|
||||
<style>
|
||||
body {background:#0e0e10;color:#eee;font-family:Segoe UI,Arial;margin:20px;}
|
||||
.box{background:#1a1a1d;border:1px solid #333;border-radius:10px;padding:12px;margin-top:10px;}
|
||||
h2{color:#3fa9f5;}
|
||||
input,select,button{background:#202124;color:#eee;border:1px solid #333;border-radius:6px;padding:6px 8px;margin:2px;}
|
||||
button:hover{background:#3fa9f5;color:#000;}
|
||||
table{border-collapse:collapse;width:100%;margin-top:10px;}
|
||||
th,td{border:1px solid #333;padding:6px;}
|
||||
tr:nth-child(even){background:#171717;}
|
||||
.msg{padding:8px;margin:8px 0;border-radius:6px;}
|
||||
.ok{background:#12391f;color:#4caf50;}
|
||||
.err{background:#3a1515;color:#ffb3b3;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>💻 Matrix Admin - Xupas</h2>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="msg <?=str_starts_with($msg,'❌')?'err':'ok'?>"><?=htmlspecialchars($msg)?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box">
|
||||
<h3>➕ Criar Utilizador</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="add_user">
|
||||
<input type="text" name="user" placeholder="nome" required>
|
||||
<input type="password" name="pass" placeholder="senha" required>
|
||||
<button>Criar</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3>👥 Utilizadores</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Admin</th>
|
||||
<th>Ativo</th>
|
||||
<th>Último Login</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
<?php foreach($users as $u):
|
||||
$n = $u['name'] ?? $u['user_id'] ?? '—';
|
||||
$seen = isset($u['last_seen_ts']) ? date('Y-m-d H:i:s', (int)($u['last_seen_ts'] / 1000)) : '—';
|
||||
?>
|
||||
<tr>
|
||||
<td><?=$n?></td>
|
||||
<td><?=!empty($u['admin'])?'🟢':'⚫'?></td>
|
||||
<td><?=!empty($u['deactivated'])?'❌':'✅'?></td>
|
||||
<td><?=$seen?></td>
|
||||
<td>
|
||||
<!-- Alterar senha -->
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="change_pass">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<input type="password" name="pass" placeholder="nova" style="width:90px">
|
||||
<button>🔑</button>
|
||||
</form>
|
||||
|
||||
<!-- Tornar admin / remover admin -->
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="<?=!empty($u['admin'])?'revoke_admin':'make_admin'?>">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button><?=!empty($u['admin'])?'⬇️':'⬆️'?></button>
|
||||
</form>
|
||||
|
||||
<!-- Ativar / desativar -->
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="<?=!empty($u['deactivated'])?'reactivate_user':'deactivate_user'?>">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button><?=!empty($u['deactivated'])?'♻️':'🚫'?></button>
|
||||
</form>
|
||||
|
||||
<!-- Apagar user -->
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Apagar <?=$n?>?');">
|
||||
<input type="hidden" name="action" value="delete_user">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button>🗑️</button>
|
||||
</form>
|
||||
|
||||
<!-- NOVO: apagar mensagens do user -->
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Apagar TODAS mensagens de <?=$n?>?');">
|
||||
<input type="hidden" name="action" value="erase_messages">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button>🧹</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- NOVO: limpar metadados gerais -->
|
||||
<div class="box">
|
||||
<h3>⚡ Limpar metadados gerais</h3>
|
||||
<form method="post" onsubmit="return confirm('⚠️ Isto apagará logs e metadados, mas não afetará utilizadores ou mensagens. Continuar?');">
|
||||
<input type="hidden" name="action" value="clear_metadata">
|
||||
<button>🗑️ Limpar tudo</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<?php declare(strict_types=1);
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
// ==============================
|
||||
// ⚙️ CONFIGURAÇÃO
|
||||
// ==============================
|
||||
$MATRIX_BASE = "https://matrix.xupas.mywire.org";
|
||||
$SERVER_NAME = "matrix.xupas.mywire.org";
|
||||
$TOKEN_FILE = "/var/www/secure/matrix_token.txt";
|
||||
|
||||
$ADMIN_TOKEN = is_readable($TOKEN_FILE) ? trim(file_get_contents($TOKEN_FILE)) : '';
|
||||
|
||||
$DEBUG_LOG = "/tmp/matrix_debug.log";
|
||||
|
||||
// ==============================
|
||||
// 📝 DEBUG HELPER
|
||||
// ==============================
|
||||
function dbg($txt) {
|
||||
global $DEBUG_LOG;
|
||||
file_put_contents($DEBUG_LOG, "[".date('Y-m-d H:i:s')."] $txt\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 🔧 API CALL
|
||||
// ==============================
|
||||
function call_api(string $method, string $url, ?array $data = null, ?string $token = null): array {
|
||||
dbg("API CALL: $method $url DATA=" . json_encode($data));
|
||||
$ch = curl_init($url);
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($token) $headers[] = "Authorization: Bearer $token";
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
]);
|
||||
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
dbg("API RESP: HTTP=$code BODY=$resp");
|
||||
curl_close($ch);
|
||||
return [$code, $resp];
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 🧼 USERNAME CLEANER FIX
|
||||
// ==============================
|
||||
function full_user(string $name, string $server): string {
|
||||
dbg("RAW POST USER=[".$name."] HEX=[".bin2hex($name)."]");
|
||||
$clean = trim($name);
|
||||
$clean = preg_replace('/[\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]/u', '', $clean);
|
||||
dbg("CLEAN USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
||||
if (preg_match('/^@[a-z0-9=_\-\.\/\+]+:[a-z0-9\.\-]+$/i', $clean)) return $clean;
|
||||
if ($clean === '') return '';
|
||||
if ($clean[0] !== '@') $clean = '@' . $clean;
|
||||
if (!str_contains($clean, ':')) $clean .= ':' . $server;
|
||||
dbg("FINAL USER=[".$clean."] HEX=[".bin2hex($clean)."]");
|
||||
return $clean;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 🚦 PROCESSAMENTO DE AÇÕES
|
||||
// ==============================
|
||||
$action = $_POST['action'] ?? null;
|
||||
$msg = '';
|
||||
|
||||
if ($action && $ADMIN_TOKEN) {
|
||||
|
||||
dbg("=== ACTION: $action ===");
|
||||
dbg("POST DATA: " . json_encode($_POST));
|
||||
|
||||
// criar utilizador
|
||||
if ($action === 'add_user') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$pass = trim($_POST['pass']);
|
||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||
"password" => $pass,
|
||||
"deactivated" => false,
|
||||
"admin" => false
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200 || $code == 201) ? "✅ Criado: $user" : "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// alterar password
|
||||
if ($action === 'change_pass') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$pass = trim($_POST['pass']);
|
||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/reset_password/" . urlencode($user), [
|
||||
"new_password" => $pass
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200) ? "🔑 Senha alterada para $user" : "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// admin / unadmin
|
||||
if ($action === 'make_admin' || $action === 'revoke_admin') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$is_admin = $action === 'make_admin';
|
||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||
"admin" => $is_admin
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200)
|
||||
? ($is_admin ? "⬆️ $user agora é admin" : "⬇️ $user deixou de ser admin")
|
||||
: "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// ativar / desativar
|
||||
if ($action === 'deactivate_user' || $action === 'reactivate_user') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
$active = $action === 'reactivate_user';
|
||||
[$code, $resp] = call_api("PUT", "$MATRIX_BASE/_synapse/admin/v2/users/" . urlencode($user), [
|
||||
"deactivated" => !$active
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200)
|
||||
? ($active ? "♻️ $user reativado" : "🚫 $user desativado")
|
||||
: "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// apagar user
|
||||
if ($action === 'delete_user') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/deactivate/" . urlencode($user), [
|
||||
"erase" => true
|
||||
], $ADMIN_TOKEN);
|
||||
$msg = ($code == 200 || $code == 204)
|
||||
? "🗑️ $user removido permanentemente"
|
||||
: "❌ Erro [$code]: $resp";
|
||||
}
|
||||
|
||||
// ✅ NOVA: apagar mensagens do user (mantendo o user)
|
||||
if ($action === 'erase_messages') {
|
||||
$user = full_user($_POST['user'], $SERVER_NAME);
|
||||
[$code, $resp] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/user/$user/rooms", null, $ADMIN_TOKEN);
|
||||
$rooms = json_decode($resp, true)['rooms'] ?? [];
|
||||
$errors = [];
|
||||
foreach ($rooms as $room) {
|
||||
[$c, $r] = call_api("POST", "$MATRIX_BASE/_synapse/admin/v1/purge_history/" . urlencode($room['room_id']), [
|
||||
"delete_local_events" => true,
|
||||
"reason" => "Apagadas via painel"
|
||||
], $ADMIN_TOKEN);
|
||||
if ($c != 200) $errors[] = "$room[room_id]: $r";
|
||||
}
|
||||
$msg = empty($errors) ? "🧹 Mensagens de $user apagadas" : "❌ Erros: " . implode("; ", $errors);
|
||||
}
|
||||
|
||||
// ✅ NOVA: limpar metadados gerais
|
||||
if ($action === 'clear_metadata') {
|
||||
@unlink($DEBUG_LOG);
|
||||
// aqui podes adicionar mais caminhos de logs/metadados
|
||||
$msg = "⚡ Metadados limpos!";
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 📋 LISTAGEM USERS
|
||||
// ==============================
|
||||
$users = [];
|
||||
if ($ADMIN_TOKEN) {
|
||||
$next = null;
|
||||
do {
|
||||
$url = "$MATRIX_BASE/_synapse/admin/v2/users?limit=100";
|
||||
if ($next) $url .= "&from=" . urlencode($next);
|
||||
[$code, $resp] = call_api("GET", $url, null, $ADMIN_TOKEN);
|
||||
if ($code != 200) { dbg("ERRO AO LER USERS: HTTP=$code BODY=$resp"); break; }
|
||||
$data = json_decode($resp, true);
|
||||
$chunk = $data['users'] ?? $data['results'] ?? [];
|
||||
$users = array_merge($users, $chunk);
|
||||
$next = $data['next_token'] ?? null;
|
||||
} while ($next);
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Matrix Admin - Xupas</title>
|
||||
<style>
|
||||
body {background:#0e0e10;color:#eee;font-family:Segoe UI,Arial;margin:20px;}
|
||||
.box{background:#1a1a1d;border:1px solid #333;border-radius:10px;padding:12px;margin-top:10px;}
|
||||
h2{color:#3fa9f5;}
|
||||
input,select,button{background:#202124;color:#eee;border:1px solid #333;border-radius:6px;padding:6px 8px;margin:2px;}
|
||||
button:hover{background:#3fa9f5;color:#000;}
|
||||
table{border-collapse:collapse;width:100%;margin-top:10px;}
|
||||
th,td{border:1px solid #333;padding:6px;}
|
||||
tr:nth-child(even){background:#171717;}
|
||||
.msg{padding:8px;margin:8px 0;border-radius:6px;}
|
||||
.ok{background:#12391f;color:#4caf50;}
|
||||
.err{background:#3a1515;color:#ffb3b3;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>💻 Matrix Admin - Xupas</h2>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="msg <?=str_starts_with($msg,'❌')?'err':'ok'?>"><?=htmlspecialchars($msg)?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box">
|
||||
<h3>➕ Criar Utilizador</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="add_user">
|
||||
<input type="text" name="user" placeholder="nome" required>
|
||||
<input type="password" name="pass" placeholder="senha" required>
|
||||
<button>Criar</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3>👥 Utilizadores</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Admin</th>
|
||||
<th>Ativo</th>
|
||||
<th>Último Login</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
<?php foreach($users as $u):
|
||||
$n = $u['name'] ?? $u['user_id'] ?? '—';
|
||||
$seen = isset($u['last_seen_ts']) ? date('Y-m-d H:i:s', (int)($u['last_seen_ts'] / 1000)) : '—';
|
||||
?>
|
||||
<tr>
|
||||
<td><?=$n?></td>
|
||||
<td><?=!empty($u['admin'])?'🟢':'⚫'?></td>
|
||||
<td><?=!empty($u['deactivated'])?'❌':'✅'?></td>
|
||||
<td><?=$seen?></td>
|
||||
<td>
|
||||
<!-- Alterar senha -->
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="change_pass">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<input type="password" name="pass" placeholder="nova" style="width:90px">
|
||||
<button>🔑</button>
|
||||
</form>
|
||||
|
||||
<!-- Tornar admin / remover admin -->
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="<?=!empty($u['admin'])?'revoke_admin':'make_admin'?>">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button><?=!empty($u['admin'])?'⬇️':'⬆️'?></button>
|
||||
</form>
|
||||
|
||||
<!-- Ativar / desativar -->
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="<?=!empty($u['deactivated'])?'reactivate_user':'deactivate_user'?>">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button><?=!empty($u['deactivated'])?'♻️':'🚫'?></button>
|
||||
</form>
|
||||
|
||||
<!-- Apagar user -->
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Apagar <?=$n?>?');">
|
||||
<input type="hidden" name="action" value="delete_user">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button>🗑️</button>
|
||||
</form>
|
||||
|
||||
<!-- NOVO: apagar mensagens do user -->
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Apagar TODAS mensagens de <?=$n?>?');">
|
||||
<input type="hidden" name="action" value="erase_messages">
|
||||
<input type="hidden" name="user" value="<?=$n?>">
|
||||
<button>🧹</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- NOVO: limpar metadados gerais -->
|
||||
<div class="box">
|
||||
<h3>⚡ Limpar metadados gerais</h3>
|
||||
<form method="post" onsubmit="return confirm('⚠️ Isto apagará logs e metadados, mas não afetará utilizadores ou mensagens. Continuar?');">
|
||||
<input type="hidden" name="action" value="clear_metadata">
|
||||
<button>🗑️ Limpar tudo</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
211
proxmox.php
Normal file
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
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
use PhpMqtt\Client\ConnectionSettings;
|
||||
|
||||
$config = require __DIR__.'/config.php';
|
||||
|
||||
$topic = $_POST['topic'] ?? $_GET['topic'] ?? null;
|
||||
$msg = $_POST['msg'] ?? $_GET['msg'] ?? null;
|
||||
$qos = (int)($_POST['qos'] ?? 0);
|
||||
$retain = (bool)($_POST['retain'] ?? false);
|
||||
|
||||
if (!$topic || $msg === null) {
|
||||
http_response_code(400);
|
||||
die('missing topic or msg');
|
||||
}
|
||||
|
||||
$settings = (new ConnectionSettings)
|
||||
->setUsername($config['username'])
|
||||
->setPassword($config['password'])
|
||||
->setKeepAliveInterval($config['keepalive'])
|
||||
->setConnectTimeout($config['timeout'])
|
||||
->setUseTls(false); // muda para true quando fores usar 8883/TLS
|
||||
|
||||
$client = new MqttClient($config['host'], $config['port'], $config['clientId']);
|
||||
$client->connect($settings, $config['clean']);
|
||||
$client->publish($topic, $msg, $qos, $retain);
|
||||
$client->disconnect();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok'=>true,'topic'=>$topic,'qos'=>$qos,'retain'=>$retain]);
|
||||
<?php
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
use PhpMqtt\Client\ConnectionSettings;
|
||||
|
||||
$config = require __DIR__.'/config.php';
|
||||
|
||||
$topic = $_POST['topic'] ?? $_GET['topic'] ?? null;
|
||||
$msg = $_POST['msg'] ?? $_GET['msg'] ?? null;
|
||||
$qos = (int)($_POST['qos'] ?? 0);
|
||||
$retain = (bool)($_POST['retain'] ?? false);
|
||||
|
||||
if (!$topic || $msg === null) {
|
||||
http_response_code(400);
|
||||
die('missing topic or msg');
|
||||
}
|
||||
|
||||
$settings = (new ConnectionSettings)
|
||||
->setUsername($config['username'])
|
||||
->setPassword($config['password'])
|
||||
->setKeepAliveInterval($config['keepalive'])
|
||||
->setConnectTimeout($config['timeout'])
|
||||
->setUseTls(false); // muda para true quando fores usar 8883/TLS
|
||||
|
||||
$client = new MqttClient($config['host'], $config['port'], $config['clientId']);
|
||||
$client->connect($settings, $config['clean']);
|
||||
$client->publish($topic, $msg, $qos, $retain);
|
||||
$client->disconnect();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok'=>true,'topic'=>$topic,'qos'=>$qos,'retain'=>$retain]);
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/api/protecao.php";
|
||||
require __DIR__ . "/api/db.php";
|
||||
|
||||
$uid = $_POST['uid'] ?? '';
|
||||
$note = $_POST['note'] ?? '';
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE contadores
|
||||
SET note = ?
|
||||
WHERE uid = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$note, $uid]);
|
||||
|
||||
<?php
|
||||
require_once __DIR__ . "/api/protecao.php";
|
||||
require __DIR__ . "/api/db.php";
|
||||
|
||||
$uid = $_POST['uid'] ?? '';
|
||||
$note = $_POST['note'] ?? '';
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE contadores
|
||||
SET note = ?
|
||||
WHERE uid = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$note, $uid]);
|
||||
|
||||
header("Location: console.php");
|
||||
@ -1,25 +1,25 @@
|
||||
<?php
|
||||
// ---------------- DB ----------------
|
||||
$dbHost = "localhost";
|
||||
$dbName = "cagalhao";
|
||||
$dbUser = "master";
|
||||
$dbPass = "master";
|
||||
|
||||
try {
|
||||
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die("Erro DB: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// ---------------- INPUT ----------------
|
||||
$uid = $_GET['uid'] ?? '';
|
||||
|
||||
if ($uid) {
|
||||
$stmt = $db->prepare("INSERT INTO comandos (uid, cmd, params, status)
|
||||
VALUES (:uid, 'PING', '{}', 'pending')");
|
||||
$stmt->execute([':uid' => $uid]);
|
||||
// echo "PING enviado para {$uid}";
|
||||
} else {
|
||||
echo "Falta o UID.";
|
||||
}
|
||||
<?php
|
||||
// ---------------- DB ----------------
|
||||
$dbHost = "localhost";
|
||||
$dbName = "cagalhao";
|
||||
$dbUser = "master";
|
||||
$dbPass = "master";
|
||||
|
||||
try {
|
||||
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8", $dbUser, $dbPass);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die("Erro DB: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// ---------------- INPUT ----------------
|
||||
$uid = $_GET['uid'] ?? '';
|
||||
|
||||
if ($uid) {
|
||||
$stmt = $db->prepare("INSERT INTO comandos (uid, cmd, params, status)
|
||||
VALUES (:uid, 'PING', '{}', 'pending')");
|
||||
$stmt->execute([':uid' => $uid]);
|
||||
// echo "PING enviado para {$uid}";
|
||||
} else {
|
||||
echo "Falta o UID.";
|
||||
}
|
||||
|
||||
50
vendor/php-mqtt/client/.github/release.yml
vendored
50
vendor/php-mqtt/client/.github/release.yml
vendored
@ -1,25 +1,25 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- octocat
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Deprecated
|
||||
labels:
|
||||
- deprecated
|
||||
- title: Removed
|
||||
labels:
|
||||
- removed
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bug
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Changed
|
||||
labels:
|
||||
- "*"
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- octocat
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Deprecated
|
||||
labels:
|
||||
- deprecated
|
||||
- title: Removed
|
||||
labels:
|
||||
- removed
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bug
|
||||
- title: Security
|
||||
labels:
|
||||
- security
|
||||
- title: Changed
|
||||
labels:
|
||||
- "*"
|
||||
|
||||
228
vpn.php
Normal file
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