156 lines
6.1 KiB
JavaScript
156 lines
6.1 KiB
JavaScript
'use strict';
|
||
/*
|
||
* token-server-ctx.js — тот же HTTP-контракт, что и token-server.js, но поверх
|
||
* chrome-launcher-ctx.js (1 процесс Chrome × N browser contexts).
|
||
*
|
||
* Отдельный файл + отдельный порт, чтобы гонять A/B рядом со старым сервером,
|
||
* не трогая боевой. Когда убедишься что лучше — в token-server.js меняется
|
||
* ОДНА строка импорта на './chrome-launcher-ctx.js', этот файл больше не нужен.
|
||
*
|
||
* Запуск на VM:
|
||
* $env:PORT="3003"; $env:INSTANCES="30"; $env:SHARDS="1"
|
||
* pm2 start token-server-ctx.js --name tokens-ctx
|
||
*/
|
||
|
||
import 'dotenv/config';
|
||
import express from 'express';
|
||
import {
|
||
getTokenForAccount, registerHlsSession, ensurePool, shutdownPool,
|
||
rotateProxies, poolStats, probeExitIps, selfTest,
|
||
} from './chrome-launcher-ctx.js';
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3003;
|
||
const API_KEY = process.env.API_KEY || 'GCHksldkkqg2254fobvmgjhtrk4o35igojvkfdothrypewohkj74365@';
|
||
|
||
async function startPool() {
|
||
try {
|
||
await ensurePool();
|
||
} catch (e) {
|
||
console.error('[pool] Ошибка инициализации пула:', e.message, '— повтор через 10с');
|
||
setTimeout(startPool, 10000);
|
||
}
|
||
}
|
||
startPool();
|
||
|
||
app.use(express.json());
|
||
|
||
app.use((req, res, next) => {
|
||
if (req.headers['x-api-key'] !== API_KEY) {
|
||
return res.status(401).json({ ok: false, error: 'unauthorized' });
|
||
}
|
||
next();
|
||
});
|
||
|
||
app.get('/health', (_req, res) => res.json({ ok: true }));
|
||
|
||
// Диагностика пула — сколько контекстов живо/занято
|
||
app.get('/stats', (_req, res) => res.json({ ok: true, ...poolStats() }));
|
||
|
||
// Реальные exit-IP по контекстам — проверка, что прокси развели IP.
|
||
// Ходит в ipify из каждого контекста, на 30 слотах занимает несколько секунд.
|
||
app.get('/probe-ips', async (_req, res) => {
|
||
try {
|
||
res.json({ ok: true, ...(await probeExitIps()) });
|
||
} catch (e) {
|
||
res.status(500).json({ ok: false, error: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /token body: { oauth, proxy? } → { ok, token, sessionId, deviceId, proxy, userAgent, headers }
|
||
// proxy (host:port:user:pass или url) — минтить на КОНКРЕТНОМ прокси аккаунта:
|
||
// форвардер слота переключается на него + reload. Без proxy — на прокси слота (как было).
|
||
app.post('/token', async (req, res) => {
|
||
const { oauth, proxy } = req.body || {};
|
||
if (!oauth) return res.status(400).json({ ok: false, error: 'oauth required' });
|
||
|
||
try {
|
||
const result = await getTokenForAccount(oauth, { timeout: 60000, proxy: proxy || null });
|
||
res.json({ ok: true, ...result });
|
||
} catch (e) {
|
||
res.status(500).json({ ok: false, error: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /selftest body: { oauth, target? }
|
||
// Минт + follow/unfollow внутри ОДНОГО контекста. integrityFailed=false → минт живой,
|
||
// значит «failed integrity check» в панели даёт маршрутизация GQL, а не контексты.
|
||
app.post('/selftest', async (req, res) => {
|
||
const { oauth, target, mode } = req.body || {};
|
||
if (!oauth) return res.status(400).json({ ok: false, error: 'oauth required' });
|
||
|
||
try {
|
||
const opts = {};
|
||
if (target) opts.target = target;
|
||
if (mode) opts.mode = mode; // fetch | sniff | both (деф both)
|
||
const result = await selfTest(oauth, opts);
|
||
res.json({ ok: true, ...result });
|
||
} catch (e) {
|
||
res.status(500).json({ ok: false, error: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /hls-register body: { oauth, channel } → { ok, status, streams }
|
||
// Дёргается панелью из дропсов (server.js _dropsRegisterHls). Ответ отдаём как есть —
|
||
// панель читает data.ok и data.streams на верхнем уровне.
|
||
app.post('/hls-register', async (req, res) => {
|
||
const { oauth, channel } = req.body || {};
|
||
if (!oauth || !channel) return res.status(400).json({ ok: false, error: 'oauth and channel required' });
|
||
|
||
try {
|
||
const result = await registerHlsSession(oauth, channel, { timeout: 45000 });
|
||
res.json(result);
|
||
} catch (e) {
|
||
res.status(500).json({ ok: false, error: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /rotate-pool — пересоздаёт контексты со свежими sticky-session (новые exit-IP)
|
||
app.post('/rotate-pool', async (_req, res) => {
|
||
console.log('[token-server-ctx] Запрос на ротацию контекстов...');
|
||
try {
|
||
await rotateProxies();
|
||
res.json({ ok: true, message: 'Контексты обновлены', ...poolStats() });
|
||
} catch (e) {
|
||
console.error('[token-server-ctx] Ошибка ротации:', e.message);
|
||
res.status(500).json({ ok: false, error: e.message });
|
||
}
|
||
});
|
||
|
||
const server = app.listen(PORT, () => {
|
||
console.log(`Token server (ctx) running on port ${PORT}`);
|
||
console.log(`API key: ${API_KEY}`);
|
||
});
|
||
|
||
server.on('error', (e) => {
|
||
if (e.code === 'EADDRINUSE') {
|
||
console.error(`[server] Порт ${PORT} занят, повтор через 3с...`);
|
||
setTimeout(() => server.listen(PORT), 3000);
|
||
} else {
|
||
console.error('[server] Ошибка:', e.message);
|
||
}
|
||
});
|
||
|
||
// Watchdog — если сервер не отвечает за 10с, выходим, pm2 поднимет заново
|
||
setInterval(async () => {
|
||
try {
|
||
const ctrl = new AbortController();
|
||
const t = setTimeout(() => ctrl.abort(), 10000);
|
||
await fetch(`http://127.0.0.1:${PORT}/health`, { headers: { 'x-api-key': API_KEY }, signal: ctrl.signal });
|
||
clearTimeout(t);
|
||
} catch (e) {
|
||
console.error('[watchdog] Сервер не отвечает, перезапуск:', e.message);
|
||
process.exit(1);
|
||
}
|
||
}, 2 * 60 * 1000);
|
||
|
||
function gracefulShutdown() {
|
||
console.log('[server] Завершение...');
|
||
shutdownPool();
|
||
server.close(() => process.exit(0));
|
||
setTimeout(() => process.exit(0), 5000).unref();
|
||
}
|
||
|
||
process.on('SIGTERM', gracefulShutdown);
|
||
process.on('SIGINT', gracefulShutdown);
|