Files
tokenserv/proxy-forwarder.js

103 lines
4.9 KiB
JavaScript
Raw Permalink 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';
/*
* 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 (_) {} },
};
}