Compare commits
3 Commits
3f31e25ae7
...
b39bbcdd0a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39bbcdd0a | ||
|
|
5489c84e6b | ||
|
|
24d35333df |
20
apps/api/scripts/test-proxy-rotation.ts
Normal file
20
apps/api/scripts/test-proxy-rotation.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { ProxyManager } from '../src/services/sat/proxy.service.js';
|
||||||
|
|
||||||
|
const manager = new ProxyManager(
|
||||||
|
process.env.SAT_PROXY_LIST || '',
|
||||||
|
(process.env.SAT_PROXY_STRATEGY as any) || 'round-robin',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Total de proxies: ${manager.getProxyCount()}`);
|
||||||
|
console.log(`Estrategia: ${process.env.SAT_PROXY_STRATEGY || 'round-robin'}`);
|
||||||
|
console.log('Próximos 10 proxies seleccionados:');
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const proxy = manager.getNextProxy();
|
||||||
|
if (!proxy) {
|
||||||
|
console.log(` ${i + 1}. (sin proxy configurado)`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${i + 1}. ${proxy.host}:${proxy.port}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
apps/api/src/services/sat/proxy.service.ts
Normal file
100
apps/api/src/services/sat/proxy.service.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
|
|
||||||
|
export interface ProxyConfig {
|
||||||
|
url: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProxyStrategy = 'round-robin' | 'random';
|
||||||
|
|
||||||
|
function parseProxyList(raw: string): ProxyConfig[] {
|
||||||
|
if (!raw.trim()) return [];
|
||||||
|
|
||||||
|
const configs: ProxyConfig[] = [];
|
||||||
|
const items = raw.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const url = new URL(item);
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
console.warn(`[ProxyManager] Protocolo no soportado, se omite: ${item}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
configs.push({
|
||||||
|
url: item,
|
||||||
|
host: url.hostname,
|
||||||
|
port: Number(url.port) || (url.protocol === 'https:' ? 443 : 80),
|
||||||
|
username: url.username || undefined,
|
||||||
|
password: url.password || undefined,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[ProxyManager] URL de proxy inválida, se omite: ${item}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProxyManager {
|
||||||
|
private proxies: ProxyConfig[];
|
||||||
|
private strategy: ProxyStrategy;
|
||||||
|
private currentIndex = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
proxyList = process.env.SAT_PROXY_LIST || '',
|
||||||
|
strategy: ProxyStrategy = (process.env.SAT_PROXY_STRATEGY as ProxyStrategy) || 'round-robin',
|
||||||
|
) {
|
||||||
|
this.proxies = parseProxyList(proxyList);
|
||||||
|
this.strategy = ['round-robin', 'random'].includes(strategy) ? strategy : 'round-robin';
|
||||||
|
|
||||||
|
if (this.proxies.length > 0) {
|
||||||
|
console.log(`[ProxyManager] ${this.proxies.length} proxy(s) configurados (estrategia: ${this.strategy})`);
|
||||||
|
} else {
|
||||||
|
console.log('[ProxyManager] No hay proxies configurados; se usará la IP directa del servidor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasProxies(): boolean {
|
||||||
|
return this.proxies.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProxyCount(): number {
|
||||||
|
return this.proxies.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextProxy(): ProxyConfig | null {
|
||||||
|
if (this.proxies.length === 0) return null;
|
||||||
|
|
||||||
|
if (this.strategy === 'random') {
|
||||||
|
return this.proxies[Math.floor(Math.random() * this.proxies.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = this.proxies[this.currentIndex];
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
createAgent(proxy: ProxyConfig): HttpsProxyAgent<string> {
|
||||||
|
return new HttpsProxyAgent(proxy.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea un agente con el siguiente proxy del pool.
|
||||||
|
* Útil cuando se quiere una nueva conexión por solicitud.
|
||||||
|
*/
|
||||||
|
createNextAgent(): HttpsProxyAgent<string> | null {
|
||||||
|
const proxy = this.getNextProxy();
|
||||||
|
if (!proxy) return null;
|
||||||
|
console.log(`[ProxyManager] Usando proxy: ${proxy.host}:${proxy.port}`);
|
||||||
|
return this.createAgent(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instancia global del proxy manager.
|
||||||
|
* Lee SAT_PROXY_LIST y SAT_PROXY_STRATEGY del entorno.
|
||||||
|
*/
|
||||||
|
export const proxyManager = new ProxyManager();
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
ServiceEndpoints,
|
ServiceEndpoints,
|
||||||
} from '@nodecfdi/sat-ws-descarga-masiva';
|
} from '@nodecfdi/sat-ws-descarga-masiva';
|
||||||
|
import { proxyManager } from './proxy.service.js';
|
||||||
|
|
||||||
export interface FielData {
|
export interface FielData {
|
||||||
cerContent: string;
|
cerContent: string;
|
||||||
@@ -47,11 +48,20 @@ export function createSatService(fielData: FielData): Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Crear cliente HTTP con timeout explícito para evitar el bug de la librería
|
// Crear cliente HTTP con timeout explícito para evitar el bug de la librería
|
||||||
// cuando ocurre un timeout de red.
|
// cuando ocurre un timeout de red. Si hay proxies configurados, se usa uno
|
||||||
|
// del pool para reducir el riesgo de bloqueo por IP del SAT.
|
||||||
|
const proxyAgent = proxyManager.createNextAgent();
|
||||||
|
if (proxyAgent) {
|
||||||
|
console.log('[SAT] Usando proxy para la conexión con el SAT');
|
||||||
|
} else {
|
||||||
|
console.log('[SAT] Sin proxy configurado; usando IP directa del servidor');
|
||||||
|
}
|
||||||
|
|
||||||
const webClient = new (HttpsWebClient as any)(
|
const webClient = new (HttpsWebClient as any)(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
SAT_WEB_CLIENT_TIMEOUT_MS,
|
SAT_WEB_CLIENT_TIMEOUT_MS,
|
||||||
|
proxyAgent,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Crear request builder con la FIEL
|
// Crear request builder con la FIEL
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/build/index.js b/build/index.js
|
diff --git a/build/index.js b/build/index.js
|
||||||
index bf7a6aafce966c4ab44ba3abb240578cc68779d6..4678df8734b098f20c7e14d8593bd4328fe759d8 100644
|
index bf7a6aafce966c4ab44ba3abb240578cc68779d6..df01102262bbe6d544a5a1b4453977be8a826d0d 100644
|
||||||
--- a/build/index.js
|
--- a/build/index.js
|
||||||
+++ b/build/index.js
|
+++ b/build/index.js
|
||||||
@@ -266,7 +266,13 @@ var ServiceConsumer = class _ServiceConsumer {
|
@@ -266,7 +266,13 @@ var ServiceConsumer = class _ServiceConsumer {
|
||||||
@@ -17,3 +17,27 @@ index bf7a6aafce966c4ab44ba3abb240578cc68779d6..4678df8734b098f20c7e14d8593bd432
|
|||||||
}
|
}
|
||||||
this.checkErrors(request, response, exception);
|
this.checkErrors(request, response, exception);
|
||||||
return response.getBody();
|
return response.getBody();
|
||||||
|
@@ -2660,10 +2666,12 @@ var HttpsWebClient = class {
|
||||||
|
_fireRequestClosure;
|
||||||
|
_fireResponseClosure;
|
||||||
|
_timeout;
|
||||||
|
- constructor(onFireRequest, onFireResponse, timeout) {
|
||||||
|
+ _agent;
|
||||||
|
+ constructor(onFireRequest, onFireResponse, timeout, agent = void 0) {
|
||||||
|
this._fireRequestClosure = onFireRequest;
|
||||||
|
this._fireResponseClosure = onFireResponse;
|
||||||
|
this._timeout = timeout;
|
||||||
|
+ this._agent = agent;
|
||||||
|
}
|
||||||
|
fireRequest(request) {
|
||||||
|
if (this._fireRequestClosure) {
|
||||||
|
@@ -2679,7 +2687,8 @@ var HttpsWebClient = class {
|
||||||
|
const options = {
|
||||||
|
method: request.getMethod(),
|
||||||
|
headers: request.getHeaders(),
|
||||||
|
- timeout: this._timeout ?? request.getTimeout() ?? void 0
|
||||||
|
+ timeout: this._timeout ?? request.getTimeout() ?? void 0,
|
||||||
|
+ agent: this._agent
|
||||||
|
};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let clientRequest;
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -6,7 +6,7 @@ settings:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@nodecfdi/sat-ws-descarga-masiva@2.0.0':
|
'@nodecfdi/sat-ws-descarga-masiva@2.0.0':
|
||||||
hash: n2q5glw3wdhkcidljfdzrkmxnq
|
hash: i4ncoh7xgprkdron5l2ech4ifm
|
||||||
path: patches/@nodecfdi__sat-ws-descarga-masiva@2.0.0.patch
|
path: patches/@nodecfdi__sat-ws-descarga-masiva@2.0.0.patch
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
@@ -39,7 +39,7 @@ importers:
|
|||||||
version: 3.2.0(luxon@3.7.2)
|
version: 3.2.0(luxon@3.7.2)
|
||||||
'@nodecfdi/sat-ws-descarga-masiva':
|
'@nodecfdi/sat-ws-descarga-masiva':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0(patch_hash=n2q5glw3wdhkcidljfdzrkmxnq)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)
|
version: 2.0.0(patch_hash=i4ncoh7xgprkdron5l2ech4ifm)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^5.22.0
|
specifier: ^5.22.0
|
||||||
version: 5.22.0(prisma@5.22.0)
|
version: 5.22.0(prisma@5.22.0)
|
||||||
@@ -3133,7 +3133,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
luxon: 3.7.2
|
luxon: 3.7.2
|
||||||
|
|
||||||
'@nodecfdi/sat-ws-descarga-masiva@2.0.0(patch_hash=n2q5glw3wdhkcidljfdzrkmxnq)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)':
|
'@nodecfdi/sat-ws-descarga-masiva@2.0.0(patch_hash=i4ncoh7xgprkdron5l2ech4ifm)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodecfdi/cfdi-core': 1.0.1
|
'@nodecfdi/cfdi-core': 1.0.1
|
||||||
'@nodecfdi/credentials': 3.2.0(luxon@3.7.2)
|
'@nodecfdi/credentials': 3.2.0(luxon@3.7.2)
|
||||||
|
|||||||
Reference in New Issue
Block a user