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}`); });