'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 (_) {} }, }; }