Загрузить файлы в «/»
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
Invoke-RestMethod http://127.0.0.1:3003/selftest -Method Post -Headers $H `
|
||||
-ContentType 'application/json' -Body '{"oauth":"01pd89xuyo232p47vzskda7ybhghha"}' | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod http://127.0.0.1:3003/health -Headers @{'x-api-key'='GCHksldkkqg2254fobvmgjhtrk4o35igojvkfdothrypewohkj74365@'}
|
||||
|
||||
|
||||
Invoke-RestMethod http://127.0.0.1:3003/selftest -Method Post -Headers @{'x-api-key'='GCHksldkkqg2254fobvmgjhtrk4o35igojvkfdothrypewohkj74365@'} -ContentType 'application/json' -Body '{"oauth":"01pd89xuyo232p47vzskda7ybhghha"}' | ConvertTo-Json
|
||||
|
||||
|
||||
|
||||
(Invoke-RestMethod http://127.0.0.1:3003/probe-ips -Headers @{'x-api-key'='GCHksldkkqg2254fobvmgjhtrk4o35igojvkfdothrypewohkj74365@'}).slots | Format-Table slot,ip,proxy
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
res-unlimited-5d47cf3e.plainproxies.com:8080:HdOnr8PeUa-session-049459-ttl-2400:qj05VPa8lFPfBTr
|
||||
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
/*
|
||||
* proxy-forwarder.js — локальный прокси-«перекладчик» на слот.
|
||||
*
|
||||
* ЗАЧЕМ. Прокси у browser-context в CDP фиксируется при создании контекста —
|
||||
* сменить его на лету нельзя, только пересоздать контекст (а это прогрев ~10с и
|
||||
* нестабильность в Node при частом destroy). Поэтому контекст навсегда смотрит в
|
||||
* ОДИН локальный адрес (127.0.0.1:PORT), а за этим адресом стоит вот этот
|
||||
* форвардер, у которого upstream (реальный резидентский прокси) можно менять
|
||||
* одной переменной. Смена IP аккаунта = setUpstream(...) + reload страницы,
|
||||
* БЕЗ пересоздания контекста → ~2-5с вместо ~10-18с.
|
||||
*
|
||||
* Держит только то, что реально нужно минту: HTTPS через CONNECT-туннель и
|
||||
* обычный HTTP forward. Upstream — HTTP-прокси с Basic-авторизацией (формат
|
||||
* плейнпрокси host:port:user:pass). SOCKS не поддерживаем: наши чат-прокси HTTP.
|
||||
*
|
||||
* Зависимостей нет — только встроенные http/net.
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import net from 'net';
|
||||
|
||||
/**
|
||||
* Поднять форвардер на 127.0.0.1:port.
|
||||
* @returns {{ server: http.Server, setUpstream: (u: {host,port,user?,pass?}|null)=>void, getUpstream: ()=>object|null, close: ()=>void }}
|
||||
*/
|
||||
export function startForwarder(port) {
|
||||
// upstream живёт в замыкании: setUpstream меняет его атомарно, новые
|
||||
// соединения (после reload страницы) идут уже через новый прокси.
|
||||
let upstream = null;
|
||||
|
||||
const authHeader = (u) =>
|
||||
u && u.user ? 'Basic ' + Buffer.from(`${u.user}:${u.pass || ''}`).toString('base64') : null;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// Обычный HTTP (не CONNECT). Твич весь на https, так что путь редкий, но
|
||||
// держим, чтобы случайный http-ресурс не ломал страницу.
|
||||
const u = upstream;
|
||||
if (!u) { res.writeHead(502); res.end('no upstream'); return; }
|
||||
const headers = { ...req.headers };
|
||||
const a = authHeader(u);
|
||||
if (a) headers['proxy-authorization'] = a;
|
||||
const preq = http.request(
|
||||
{ host: u.host, port: u.port, method: req.method, path: req.url, headers },
|
||||
(pres) => { res.writeHead(pres.statusCode || 502, pres.headers); pres.pipe(res); },
|
||||
);
|
||||
preq.on('error', () => { try { res.writeHead(502); res.end(); } catch (_) {} });
|
||||
req.pipe(preq);
|
||||
});
|
||||
|
||||
// HTTPS: браузер шлёт CONNECT host:443 — открываем туннель до цели ЧЕРЕЗ upstream.
|
||||
server.on('connect', (req, clientSocket, head) => {
|
||||
const u = upstream;
|
||||
if (!u) { try { clientSocket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n'); } catch (_) {} return; }
|
||||
|
||||
const upSock = net.connect(u.port, u.host, () => {
|
||||
let line = `CONNECT ${req.url} HTTP/1.1\r\nHost: ${req.url}\r\n`;
|
||||
const a = authHeader(u);
|
||||
if (a) line += `Proxy-Authorization: ${a}\r\n`;
|
||||
line += 'Connection: keep-alive\r\n\r\n';
|
||||
upSock.write(line);
|
||||
});
|
||||
|
||||
let header = Buffer.alloc(0);
|
||||
let piped = false;
|
||||
const onData = (d) => {
|
||||
if (piped) return;
|
||||
header = Buffer.concat([header, d]);
|
||||
const idx = header.indexOf('\r\n\r\n');
|
||||
if (idx === -1) return; // ответ upstream ещё не дочитан
|
||||
const status = header.slice(0, header.indexOf('\r\n')).toString();
|
||||
upSock.removeListener('data', onData);
|
||||
if (!/^HTTP\/1\.[01] 200/.test(status)) { // upstream отказал (авторизация/цель)
|
||||
try { clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\n`); } catch (_) {}
|
||||
try { upSock.end(); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
piped = true;
|
||||
try { clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); } catch (_) {}
|
||||
const rest = header.slice(idx + 4); // тело, прилипшее к заголовку
|
||||
if (rest.length) clientSocket.write(rest);
|
||||
if (head && head.length) upSock.write(head);
|
||||
upSock.pipe(clientSocket);
|
||||
clientSocket.pipe(upSock);
|
||||
};
|
||||
upSock.on('data', onData);
|
||||
|
||||
const kill = () => { try { upSock.destroy(); } catch (_) {} try { clientSocket.destroy(); } catch (_) {} };
|
||||
upSock.on('error', kill);
|
||||
clientSocket.on('error', kill);
|
||||
});
|
||||
|
||||
server.on('clientError', (_e, sock) => { try { sock.destroy(); } catch (_) {} });
|
||||
server.listen(port, '127.0.0.1');
|
||||
|
||||
return {
|
||||
server,
|
||||
setUpstream(u) { upstream = u; },
|
||||
getUpstream() { return upstream; },
|
||||
close() { try { server.close(); } catch (_) {} },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
'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);
|
||||
Reference in New Issue
Block a user