commit-all-changes
This commit is contained in:
parent
0d8cf8a7e5
commit
bea0c5c200
@ -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,18 +320,40 @@ $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("
|
||||
SELECT auth_nonce, auth_nonce_at
|
||||
FROM contadores
|
||||
WHERE uid=:uid
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute(['uid' => $uid]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$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, last_seen, online, blocked)
|
||||
(uid, auth_nonce, auth_nonce_at, last_seen, online, blocked)
|
||||
VALUES
|
||||
(:uid, :nonce, NOW(), 1, 1)
|
||||
(:uid, :nonce, NOW(), NOW(), 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
auth_nonce=:nonce,
|
||||
auth_nonce_at=NOW(),
|
||||
last_seen=NOW(),
|
||||
online=1
|
||||
");
|
||||
@ -275,21 +362,20 @@ $subscribe = function () use (
|
||||
|
||||
$stmt->execute([
|
||||
'uid' => $uid,
|
||||
'nonce' => $nonce
|
||||
'nonce' => $nonce,
|
||||
]);
|
||||
}
|
||||
|
||||
$mqtt->publish(
|
||||
"esp/$uid/cmd",
|
||||
json_encode([
|
||||
'cmd' => 'AUTH_CHALLENGE',
|
||||
'nonce' => $nonce
|
||||
'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) {
|
||||
|
||||
// ── connect / reconnect ────────────────────────
|
||||
|
||||
foreach ($brokerPairs as $pair) {
|
||||
|
||||
$label = $pair['label'];
|
||||
|
||||
if (
|
||||
isset($clients[$label]) &&
|
||||
$clients[$label]->isConnected()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// throttle: não tentar mais do que 1x / 5 s por broker
|
||||
if (time() < ($lastConnectAttempt[$label] ?? 0) + 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastConnectAttempt[$label] = time();
|
||||
|
||||
try {
|
||||
|
||||
logmsg(
|
||||
"Ligar recovery $recoveryHost:$recoveryPort"
|
||||
"Ligar recovery [{$label}] " .
|
||||
"{$pair['recvHost']}:{$pair['recvPort']}"
|
||||
);
|
||||
|
||||
$mqtt->connect($settings, false);
|
||||
|
||||
$subscribe();
|
||||
|
||||
logmsg(
|
||||
"RECOVERY ligado e subscrito"
|
||||
$clients[$label] = new MqttClient(
|
||||
$pair['recvHost'],
|
||||
$pair['recvPort'],
|
||||
'listener-recovery-' . $label . '-' . getmypid()
|
||||
);
|
||||
|
||||
$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
|
||||
$clients[$label]->connect(
|
||||
make_settings($pair),
|
||||
false
|
||||
);
|
||||
|
||||
if (
|
||||
main_broker_online(
|
||||
$mainHost,
|
||||
$mainPort
|
||||
)
|
||||
) {
|
||||
|
||||
logmsg(
|
||||
"MAIN broker online"
|
||||
subscribe_broker(
|
||||
$clients[$label],
|
||||
$db,
|
||||
$pair,
|
||||
$authSecret
|
||||
);
|
||||
|
||||
logmsg("Mandar switch_primary");
|
||||
// primeira verificação do main broker após 10 s
|
||||
$nextCheck[$label] = time() + 10;
|
||||
|
||||
send_switch_primary(
|
||||
$mqtt,
|
||||
$db
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
logmsg(
|
||||
"MAIN broker ainda offline",
|
||||
LOG_DEBUG
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$mqtt->disconnect();
|
||||
logmsg("[{$label}] ligado e subscrito");
|
||||
|
||||
} catch (Throwable $e) {
|
||||
|
||||
logmsg(
|
||||
"ERRO recovery: " .
|
||||
$e->getMessage(),
|
||||
"[{$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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
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>
|
||||
|
||||
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';
|
||||
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