Files
tokenserv/token-server-ctx.js
T

156 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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);