@extra/proxy-router
Version:
A plugin for playwright & puppeteer to route proxies dynamically.
191 lines • 9.17 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProxyRouterStandalone = exports.ProxyRouter = void 0;
const proxy_chain_1 = require("proxy-chain");
const port_1 = __importDefault(require("./utils/port"));
const stats_1 = require("./stats");
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('puppeteer-extra:proxy-router');
const debugVerbose = debug.extend('verbose');
const warn = console.warn.bind(console, `\n[proxy-router] %s`); // Preserves line numbers
class ProxyRouter {
constructor(opts = {}) {
var _a, _b;
this.isListening = false;
/** Internal list of failed connections to only print the same connection issue once */
this.failedConnections = [];
const proxyServerOpts = Object.assign(Object.assign({}, opts.proxyServerOpts), { prepareRequestFunction: this.handleProxyServerRequest.bind(this) });
proxyServerOpts.port = proxyServerOpts.port || 2800;
this.proxies = opts.proxies || {};
this.routeByHost = opts.routeByHost || null;
this.proxyServer = new proxy_chain_1.Server(proxyServerOpts);
this.collectStats = (_a = opts.collectStats) !== null && _a !== void 0 ? _a : true;
this.stats = new stats_1.ProxyRouterStats(this.proxyServer);
this.muteProxyErrors = (_b = opts.muteProxyErrors) !== null && _b !== void 0 ? _b : false;
this.muteProxyErrorsForHost = opts.muteProxyErrorsForHost || [];
debug('initialized', opts);
// Emitted when HTTP connection is closed
this.proxyServer.on('connectionClosed', ({ connectionId, stats }) => {
if (stats && this.collectStats) {
this.stats.addStats(connectionId, stats);
}
debugVerbose(`Connection ${connectionId} closed`);
});
// Emitted when a HTTP request fails
this.proxyServer.on('requestFailed', ({ request, error }) => {
if (!this.muteProxyErrors) {
warn('Request failed:', request.url, error);
}
});
// Emitted in case of a upstream proxy error (which can mean various things)
this.proxyServer.on('proxyAuthenticationFailed', ({ connectionId, str: errorStr, }) => {
// resolve the affected host and proxy
const { host, proxy } = this.stats.connectionLog.find(({ id }) => id === connectionId) || {};
const proxyUrl = !!proxy ? this.getProxyForName(proxy) : null;
const info = [errorStr];
info.push("This error can be thrown if a resource on a site simply can't be accessed (often temporarily), in this case this can be ignored.", ` - To not have errors like this printed to the console you can set 'muteProxyErrors: true' ${!!host ? `or 'muteProxyErrorsForHost: ["${host}"]'` : ''}`, 'It can also indicate incorrect proxy credentials or that the target host is blocked by the proxy.', ' - Make sure the provided proxy string and credentials are correct and the site is not blocked by the proxy (or vice versa).', " - In case the site is blocked by the proxy: Use 'routeByHost' to route the host through a different proxy or as 'DIRECT' or 'ABORT'.");
if (host && proxy) {
info.push('', `Affected target host: "${host}"`, `Affected proxy name: "${proxy}"`);
}
if (proxyUrl) {
info.push(`Affected proxy URL: "${proxyUrl}"`);
info.push('', `To test the proxy with curl: curl -v --proxy '${proxyUrl}' 'https://${host}'`, '');
if (!`${proxyUrl}`.includes('http://')) {
info.push('PS: Did you forget to prefix the proxy with "http://"?');
}
}
const probablyNoise = errorStr.includes('authenticate') && errorStr.includes('522');
const isMuted = this.muteProxyErrors || this.muteProxyErrorsForHost.includes(host);
const alreadySeen = !!this.failedConnections.find((entry) => entry.host === host && entry.proxy === proxy);
const logger = probablyNoise || isMuted || alreadySeen ? debug : warn;
logger(info.join('\n'));
if (host && proxy) {
this.failedConnections.push({ host, proxy });
}
});
// Resurface some errors that proxy-chain seems to swallow
this.proxyServer.log = (function (originalMethod, context) {
return function (connectionId, str) {
if (`${str}`.includes('Failed to authenticate upstream proxy')) {
context.emit('proxyAuthenticationFailed', {
connectionId,
str,
});
}
if (`${str}`.includes('Error: Invalid "upstreamProxyUrl" provided')) {
context.emit('proxyAuthenticationFailed', {
connectionId,
str,
});
}
if (`${str}`.includes('Failed to connect to upstream proxy')) {
context.emit('proxyAuthenticationFailed', {
connectionId,
str,
});
}
originalMethod.apply(context, [connectionId, str]);
};
})(this.proxyServer.log, this.proxyServer);
}
/** Proxy server URL of the local proxy server used for routing */
get proxyServerUrl() {
var _a;
const port = (_a = this.proxyServer) === null || _a === void 0 ? void 0 : _a.port;
if (!port || !this.isListening) {
return;
}
return `http://localhost:${port}`;
}
get effectiveProxies() {
return Object.assign({ DIRECT: null }, (this.proxies || {}));
}
/** Start the local proxy server and accept connections */
async listen() {
debug('starting server..');
if (this.serverStartPromise) {
debug('server start promise exists already');
return this.serverStartPromise;
}
this.serverStartPromise = new Promise(async (resolve) => {
if (this.isListening) {
debug('server listening already');
return resolve(this.proxyServer.port);
}
const desiredPort = this.proxyServer.port;
debug('finding available port', { desiredPort });
const availablePort = await (0, port_1.default)({ port: desiredPort });
debug('availablePort:', availablePort);
this.proxyServer.port = availablePort;
this.proxyServer.listen((err) => {
if (err === null) {
debug(`server listening on port ${this.proxyServer.port}`);
this.isListening = true;
return resolve(this.proxyServer.port);
}
warn('Unable to start local server:', err);
});
});
return this.serverStartPromise;
}
/** Stop the local proxy server */
async close() {
debug('closing..');
return new Promise((resolve) => {
this.proxyServer.close(true, (err) => {
if (err === null) {
debug('closed without error');
return resolve(null);
}
debug('closed with error', err);
return resolve(err);
});
});
}
getProxyForName(name) {
return this.effectiveProxies[name];
}
/** Handle requests to the proxy server */
async handleProxyServerRequest({ request, hostname: host, port, connectionId, isHttp, }) {
let proxyName = 'DEFAULT';
if (!!this.routeByHost) {
const fnResult = await this.routeByHost({ host, isHttp, port });
if (typeof fnResult === 'string' && !!fnResult) {
proxyName = fnResult;
}
}
if (this.collectStats) {
this.stats.addConnection(connectionId, proxyName, host);
}
let proxyUrl = this.getProxyForName(proxyName);
debugVerbose('handleProxyServerRequest', host, proxyName, redactProxyUrl(proxyUrl));
if (proxyName === 'ABORT') {
throw new proxy_chain_1.RequestError('Request aborted', 400);
}
if (!proxyUrl && proxyUrl !== null) {
warn(`No proxy configured for proxy name "${proxyName}" - configuration error?`);
proxyUrl = null;
}
return {
upstreamProxyUrl: proxyUrl,
};
}
}
exports.ProxyRouter = ProxyRouter;
function redactProxyUrl(input) {
if (!input || typeof input !== 'string') {
return `${input}`;
}
try {
return (0, proxy_chain_1.redactUrl)(input);
}
catch (err) {
return `${input}`;
}
}
/** Standalone proxy router not requiring plugin events */
exports.ProxyRouterStandalone = ProxyRouter;
//# sourceMappingURL=router.js.map