Dashboax / server.js
Suvarii's picture
Update server.js
4361e24 verified
Raw
History Blame Contribute Delete
18.2 kB
const express = require('express');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.static('public'));
const AXIOS_CONFIG = {
timeout: 4000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json'
}
};
// Botların kendi tarafında açtığı endpoint yolları - tek yerden yönetilir.
// bot.js tarafında path isimleri değişirse SADECE burayı güncellemen yeterli.
const BOT_ENDPOINTS = {
status: '/status',
start: '/bot/start',
stop: '/bot/stop',
restart: '/bot/restart', // sadece mineflayer bağlantısını yeniden kurar (soft restart)
fullRestart: '/restart', // TÜM projeyi (konteyneri) sıfırdan başlatır (hard restart)
logs: '/logs', // sistem logları
messages: '/messages', // oyun içi sohbet akışı
sendmsg: '/bot/sendmsg', // oyun içine chat mesajı gönderme
isGo: '/bot/is-go', // /is go komutu
skyblock: '/bot/skyblock', // /skyblock komutu
besleyici: '/bot/besleyici-kontrol',
ciftci: '/bot/ciftci-satis',
kazici: '/bot/kazici-kontrol',
checkFarmer: '/bot/checkFarmer', // kazıcının manuel (anlık) tetiklenmesi
antibotmove: '/bot/antibotmove',
sendmoney: '/bot/sendmoney', // botun tüm bakiyesini verilen nick'e gönderir
order: '/bot/order', // belirtilen item'ı (diamond/emerald) verilen nick'e siparişle satış
storageInfo: '/storageInfo' // depo (elmas/zümrüt/iron block) doluluk bilgisi
};
// ---------------------------------------------------------------------------
// bots.json önbelleği - dosya değişmediği sürece diskten tekrar okumaz.
// Onlarca/yüzlerce bot olsa bile her /api/stats isteğinde gereksiz disk I/O
// yapılmasını engeller.
// ---------------------------------------------------------------------------
let botListCache = { urls: [], mtimeMs: 0 };
function getBotList() {
const filePath = path.join(__dirname, 'bots.json');
try {
const stat = fs.statSync(filePath);
if (stat.mtimeMs !== botListCache.mtimeMs) {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
botListCache = { urls: Array.isArray(parsed) ? parsed : [], mtimeMs: stat.mtimeMs };
}
} catch (e) {
console.error("[-] bots.json okunurken hata:", e.message);
// Okuma hatasında son bilinen iyi listeyi koru, tamamen boşa düşürme.
}
return botListCache.urls;
}
function normalizeUrl(u) {
return (u || "").trim().replace(/\/+$/, "");
}
// Proxy uçlarının sadece bots.json'da tanımlı adreslere istek atabilmesi için
// (aksi halde panel açık bir SSRF proxy'sine dönüşür: ?url= ile herhangi bir
// iç ağ adresine istek attırılabilir).
function isKnownBotUrl(url) {
const clean = normalizeUrl(url);
if (!clean) return false;
return getBotList().some(u => normalizeUrl(u) === clean);
}
// ---------------------------------------------------------------------------
// IP doğrulama: "Bilinmiyor", boş, null gibi değerlerin gerçek bir IP'ymiş
// gibi gruplanıp kendi başına sahte bir grup açmasını engellemek için.
// ---------------------------------------------------------------------------
const IPV4_RE = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
const IPV6_RE = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
const INVALID_IP_TOKENS = new Set(["bilinmiyor", "unknown", "n/a", "na", "null", "undefined", "---", "yok", ""]);
function isValidIp(ip) {
if (!ip || typeof ip !== 'string') return false;
const v = ip.trim();
if (INVALID_IP_TOKENS.has(v.toLowerCase())) return false;
return IPV4_RE.test(v) || IPV6_RE.test(v);
}
function parseUptimeToSeconds(str) {
if (!str || typeof str !== 'string') return 0;
let total = 0;
const d = str.match(/(\d+)\s*Gün/i);
const h = str.match(/(\d+)\s*Saat/i);
const m = str.match(/(\d+)\s*Dakika/i);
const s = str.match(/(\d+)\s*Saniye/i);
if (d) total += parseInt(d[1], 10) * 86400;
if (h) total += parseInt(h[1], 10) * 3600;
if (m) total += parseInt(m[1], 10) * 60;
if (s) total += parseInt(s[1], 10);
return total;
}
// ---------------------------------------------------------------------------
// Bot başına son bilinen GEÇERLİ bağlantı bilgisi (process belleğinde tutulur).
//
// Mantık (kullanıcının istediği kural):
// - Bu turda gelen IP geçerliyse -> her zaman onu kullan ve belleği güncelle.
// - Gelen IP geçersiz/boşsa AMA uptime bir önceki turdan düşük DEĞİLSE
// (yani bot aynı oturuma/bağlantıya devam ediyor) -> son bilinen geçerli
// IP'yi kullanmaya devam et. Uptime sıfırlanmadıysa eski IP hâlâ geçerlidir.
// - Uptime bir önceki turdan düşükse (bot yeniden başlamış/yeniden bağlanmış
// demektir) -> eski IP artık güvenilir değildir, sıfırdan başla.
// ---------------------------------------------------------------------------
const botStateMemory = new Map(); // url -> { ip, geo, type, uptimeSeconds }
function resolveConnectionInfo(url, liveData, fetchSucceeded) {
const prev = botStateMemory.get(url);
if (!fetchSucceeded) {
// Bota hiç ulaşılamadı: yeni/güvenilir bir veri yok, uptime de bilinmiyor.
// Bunu "oturum sıfırlandı" gibi yorumlamıyoruz; son bilineni aynen koruyoruz
// ki geçici bir ağ/timeout hatası IP'yi "Bilinmiyor"a düşürmesin.
return prev
? { ip: prev.ip, geo: prev.geo, type: prev.type }
: { ip: null, geo: null, type: null };
}
const currentUptime = parseUptimeToSeconds(liveData.bot_uptime);
const incomingValid = isValidIp(liveData.connection_ip);
const uptimeReset = !prev || currentUptime < prev.uptimeSeconds;
let ip, geo, type;
if (incomingValid) {
ip = liveData.connection_ip.trim();
geo = liveData.connection_geo || null;
type = liveData.connection_type || null;
} else if (prev && !uptimeReset) {
ip = prev.ip;
geo = prev.geo;
type = prev.type;
} else {
ip = null;
geo = liveData.connection_geo || null;
type = liveData.connection_type || null;
}
botStateMemory.set(url, { ip, geo, type, uptimeSeconds: currentUptime });
return { ip, geo, type };
}
// ---------------------------------------------------------------------------
// Depo (storageInfo) önbelleği - her /api/stats turunda (10sn'de bir) bota
// tekrar tekrar istek atmak yerine, bot başına en fazla 30 saniyede bir
// /storageInfo çağrılır. Sonuç process belleğinde url -> {data, lastFetchMs}
// olarak tutulur ve her /api/stats yanıtında cache'teki son veri döndürülür.
//
// Materyallerden biri (ya da hepsi) cevapta hiç gelmese bile burada hiçbir
// varsayım/doğrulama yapılmıyor - obje ne geldiyse olduğu gibi cache'leniyor.
// Eksik materyali göstermek/gizlemek tamamen frontend'in işi, backend sadece
// "en son ne aldıysak onu" saklar.
// ---------------------------------------------------------------------------
const STORAGE_REFRESH_MS = 30 * 1000; // 30 saniye
const storageCache = new Map(); // url -> { data, lastFetchMs }
async function refreshStorageIfNeeded(url) {
const now = Date.now();
const cached = storageCache.get(url);
if (cached && (now - cached.lastFetchMs) < STORAGE_REFRESH_MS) {
return; // henüz 30 saniye dolmadı, mevcut cache'i koru
}
// Zamanı geldi (ya da hiç çekilmemiş). Önce "son denenme zamanı"nı hemen
// güncelliyoruz ki aynı anda gelen paralel /api/stats turları botu
// gereksiz yere ikinci kez storageInfo'ya boğmasın.
storageCache.set(url, { data: cached ? cached.data : null, lastFetchMs: now });
try {
const response = await axios.get(`${url}${BOT_ENDPOINTS.storageInfo}`, AXIOS_CONFIG);
storageCache.set(url, { data: response.data || null, lastFetchMs: Date.now() });
} catch (err) {
// Çekilemedi: eski veriyi (varsa) koru, sadece zaman damgasını güncelle -
// böylece bir sonraki deneme yine 5 dakika sonra olur, bot spam edilmez.
storageCache.set(url, { data: cached ? cached.data : null, lastFetchMs: Date.now() });
}
}
app.get('/api/stats', async (req, res) => {
const urls = getBotList();
const stats = await Promise.all(urls.map(async (rawUrl) => {
const url = normalizeUrl(rawUrl);
const fallbackName = url.split('//')[1]?.split('.')[0] || "Bilinmeyen Bot";
let data = {};
let fetchSucceeded = false;
try {
const response = await axios.get(`${url}${BOT_ENDPOINTS.status}`, AXIOS_CONFIG);
data = response.data || {};
fetchSucceeded = true;
} catch (err) {
fetchSucceeded = false;
}
const conn = resolveConnectionInfo(url, data, fetchSucceeded);
// Depo verisi status'tan bağımsız bir endpoint olduğu için ayrı
// yönetiliyor - bot /status'a cevap vermese bile en son bilinen
// depo verisi elde varsa panelde gösterilmeye devam eder.
if (fetchSucceeded) {
await refreshStorageIfNeeded(url);
}
const storageEntry = storageCache.get(url);
return {
url,
ready: fetchSucceeded ? (data.ready ?? false) : false,
username: data.username || fallbackName,
connection_type: conn.type || "N/A",
connection_ip: conn.ip, // artık ya gerçek/doğrulanmış bir IP, ya da null - asla sahte "Bilinmiyor" string'i değil
connection_geo: conn.geo || (fetchSucceeded ? "Bilinmiyor" : "Erişim Yok"),
bot_uptime: fetchSucceeded ? (data.bot_uptime || "---") : "---",
last_minyon_check: fetchSucceeded ? (data.last_minyon_check || "---") : "---",
last_minyon_feed: fetchSucceeded ? (data.last_minyon_feed || "---") : "---",
next_scheduled_check: fetchSucceeded ? (data.next_scheduled_check || "---") : "---",
storage: storageEntry ? storageEntry.data : null
};
}));
// Sadece gerçekten doğrulanmış IP'ler grup oluşturur; "Bilinmiyor" artık
// buraya hiç girmiyor çünkü connection_ip zaten null olarak geliyor.
const ipGroups = stats.reduce((acc, bot) => {
if (bot.connection_ip) {
const ip = bot.connection_ip;
if (!acc[ip]) acc[ip] = { count: 0, type: bot.connection_type || "SSH" };
acc[ip].count++;
}
return acc;
}, {});
res.json({ stats, ipGroups });
});
// ---------------------------------------------------------------------------
// Bot aksiyon proxy'leri (start / stop / restart / is-go / skyblock / ...) -
// hepsi aynı kalıba uyar: parametre almadan botun ilgili GET endpoint'ine
// istek atar. SSRF koruması: url parametresi bots.json'da tanımlı olmalı,
// aksi halde 403.
// ---------------------------------------------------------------------------
function registerActionProxy(routePath, botEndpoint) {
app.get(routePath, async (req, res) => {
const targetUrl = req.query.url;
if (!targetUrl) return res.status(400).json({ success: false, error: "URL parametresi eksik." });
if (!isKnownBotUrl(targetUrl)) return res.status(403).json({ success: false, error: "Tanımlı olmayan bot URL'si." });
try {
const cleanUrl = normalizeUrl(targetUrl);
const response = await axios.get(`${cleanUrl}${botEndpoint}`, AXIOS_CONFIG);
res.json({ success: true, data: response.data });
} catch (err) {
res.status(500).json({ success: false, error: "Komut iletilemedi." });
}
});
}
registerActionProxy('/api/proxy/start', BOT_ENDPOINTS.start);
registerActionProxy('/api/proxy/stop', BOT_ENDPOINTS.stop);
registerActionProxy('/api/proxy/restart', BOT_ENDPOINTS.restart);
registerActionProxy('/api/proxy/full-restart', BOT_ENDPOINTS.fullRestart);
registerActionProxy('/api/proxy/is-go', BOT_ENDPOINTS.isGo);
registerActionProxy('/api/proxy/skyblock', BOT_ENDPOINTS.skyblock);
registerActionProxy('/api/proxy/besleyici', BOT_ENDPOINTS.besleyici);
registerActionProxy('/api/proxy/ciftci', BOT_ENDPOINTS.ciftci);
registerActionProxy('/api/proxy/kazici', BOT_ENDPOINTS.kazici);
registerActionProxy('/api/proxy/checkfarmer', BOT_ENDPOINTS.checkFarmer); // kazıcının manuel/anlık tetiklenmesi
registerActionProxy('/api/proxy/antibotmove', BOT_ENDPOINTS.antibotmove);
// Chat'e mesaj gönderme - ekstra "message" query parametresi taşıdığı için
// generic registerActionProxy kalıbına uymuyor, kendi route'u var.
app.get('/api/proxy/sendmsg', async (req, res) => {
const targetUrl = req.query.url;
const message = req.query.message;
if (!targetUrl) return res.status(400).json({ success: false, error: "URL parametresi eksik." });
if (!isKnownBotUrl(targetUrl)) return res.status(403).json({ success: false, error: "Tanımlı olmayan bot URL'si." });
if (!message || typeof message !== 'string' || !message.trim()) {
return res.status(400).json({ success: false, error: "Mesaj boş olamaz." });
}
try {
const cleanUrl = normalizeUrl(targetUrl);
const response = await axios.get(`${cleanUrl}${BOT_ENDPOINTS.sendmsg}`, {
...AXIOS_CONFIG,
params: { message }
});
res.json({ success: true, data: response.data });
} catch (err) {
res.status(500).json({ success: false, error: "Mesaj iletilemedi." });
}
});
// Para gönderme - botun tüm bakiyesini verilen "nick" hesabına gönderir.
// Ekstra "nick" query parametresi taşıdığı için generic registerActionProxy
// kalıbına uymuyor, sendmsg ile aynı desende kendi route'u var.
// SSRF koruması burada da isKnownBotUrl ile aynı şekilde uygulanır.
app.get('/api/proxy/sendmoney', async (req, res) => {
const targetUrl = req.query.url;
const nick = req.query.nick;
if (!targetUrl) return res.status(400).json({ success: false, error: "URL parametresi eksik." });
if (!isKnownBotUrl(targetUrl)) return res.status(403).json({ success: false, error: "Tanımlı olmayan bot URL'si." });
if (!nick || typeof nick !== 'string' || !nick.trim()) {
return res.status(400).json({ success: false, error: "Nick boş olamaz." });
}
try {
const cleanUrl = normalizeUrl(targetUrl);
const response = await axios.get(`${cleanUrl}${BOT_ENDPOINTS.sendmoney}`, {
...AXIOS_CONFIG,
params: { nick: nick.trim() }
});
res.json({ success: true, data: response.data });
} catch (err) {
res.status(500).json({ success: false, error: "Para gönderme komutu iletilemedi." });
}
});
// Order Satış - botun deposundaki belirtilen item'ı (sadece "diamond" veya
// "emerald") verilen "nick" hesabına satış siparişi olarak gönderir.
// nick ve item aynı anda taşıdığı için generic registerActionProxy kalıbına
// uymuyor, sendmsg/sendmoney ile aynı desende kendi route'u var. item
// backend tarafında da doğrulanır ki frontend atlanıp keyfi bir değer
// (örn. başka bir eşya adı) proxy'lenemesin.
const ALLOWED_ORDER_ITEMS = new Set(['diamond', 'emerald']);
app.get('/api/proxy/order', async (req, res) => {
const targetUrl = req.query.url;
const nick = req.query.nick;
const item = req.query.item;
if (!targetUrl) return res.status(400).json({ success: false, error: "URL parametresi eksik." });
if (!isKnownBotUrl(targetUrl)) return res.status(403).json({ success: false, error: "Tanımlı olmayan bot URL'si." });
if (!nick || typeof nick !== 'string' || !nick.trim()) {
return res.status(400).json({ success: false, error: "Nick boş olamaz." });
}
if (!item || !ALLOWED_ORDER_ITEMS.has(String(item).toLowerCase())) {
return res.status(400).json({ success: false, error: "Item sadece 'diamond' veya 'emerald' olabilir." });
}
try {
const cleanUrl = normalizeUrl(targetUrl);
const response = await axios.get(`${cleanUrl}${BOT_ENDPOINTS.order}`, {
...AXIOS_CONFIG,
params: { nick: nick.trim(), item: String(item).toLowerCase() }
});
res.json({ success: true, data: response.data });
} catch (err) {
res.status(500).json({ success: false, error: "Order Satış komutu iletilemedi." });
}
});
// Sistem logları (yeni /logs endpoint'i - logger.js / getRecentLogs çıktısı)
app.get('/api/proxy/logs', async (req, res) => {
const targetUrl = req.query.url;
if (!targetUrl) return res.status(400).json({ error: "URL parametresi eksik." });
if (!isKnownBotUrl(targetUrl)) return res.status(403).json({ error: "Tanımlı olmayan bot URL'si." });
try {
const cleanUrl = normalizeUrl(targetUrl);
const response = await axios.get(`${cleanUrl}${BOT_ENDPOINTS.logs}`, AXIOS_CONFIG);
res.json(response.data);
} catch (err) {
res.status(500).json({ error: "Sistem logları çekilemedi." });
}
});
// Chat/mesaj akışı (mevcut /messages endpoint'i - oyun içi sohbet)
app.get('/api/proxy/messages', async (req, res) => {
const targetUrl = req.query.url;
if (!targetUrl) return res.status(400).json({ error: "URL parametresi eksik." });
if (!isKnownBotUrl(targetUrl)) return res.status(403).json({ error: "Tanımlı olmayan bot URL'si." });
try {
const cleanUrl = normalizeUrl(targetUrl);
const response = await axios.get(`${cleanUrl}${BOT_ENDPOINTS.messages}`, AXIOS_CONFIG);
res.json(response.data);
} catch (err) {
res.status(500).json({ error: "Sohbet logları çekilemedi." });
}
});
const PORT = process.env.PORT || 7860;
app.listen(PORT, () => {
console.log(`[+] Sunucu aktif, port: ${PORT}`);
});