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