File size: 18,163 Bytes
e2de00c
 
fe3279a
 
 
 
e2de00c
fe3279a
75fd575
fe3279a
e138464
 
 
93b230e
e138464
 
 
e2de00c
cdca1e3
 
 
 
 
 
962aad8
 
 
 
 
 
 
 
 
 
1b6da43
573fc66
1b6da43
4361e24
1b6da43
cdca1e3
 
 
 
 
 
 
 
e138464
cdca1e3
e138464
cdca1e3
 
 
 
e138464
93b230e
 
cdca1e3
93b230e
cdca1e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdd77de
 
1b6da43
 
c224bdf
1b6da43
 
 
 
 
 
 
 
c224bdf
1b6da43
 
 
 
 
 
 
c224bdf
1b6da43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93b230e
e138464
ff02112
93b230e
cdca1e3
 
ff02112
cdca1e3
 
93b230e
cdca1e3
 
 
93b230e
cdca1e3
e2de00c
cdca1e3
 
 
1b6da43
 
 
 
 
 
 
 
cdca1e3
 
 
 
 
 
 
 
 
 
1b6da43
 
cdca1e3
e2de00c
 
cdca1e3
 
93b230e
9f102e3
 
 
 
 
93b230e
 
 
 
 
 
cdca1e3
962aad8
 
 
 
cdca1e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962aad8
 
 
 
 
 
1b6da43
962aad8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdca1e3
573fc66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4361e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdca1e3
 
9f102e3
 
cdca1e3
9f102e3
cdca1e3
 
 
9f102e3
cdca1e3
9f102e3
 
 
cdca1e3
9f102e3
 
 
cdca1e3
9f102e3
cdca1e3
 
9f102e3
 
cdca1e3
9f102e3
 
 
 
 
 
4361e24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
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}`);
});