-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxies-cors.js
More file actions
191 lines (175 loc) · 10.1 KB
/
Copy pathproxies-cors.js
File metadata and controls
191 lines (175 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// ========================================
// ARSENAL DE PROXIES CORS — módulo dedicado (separa los proxies de la lógica).
// Provee una lista extensa de proxies CORS públicos Y un sistema de SALUD que
// reordena dinámicamente: los que responden suben al frente, los que fallan
// bajan. Así una búsqueda usa primero lo que está vivo y rápido, en vez de
// recorrer una lista estática gigante de endpoints muertos.
//
// Cada proxy se describe con:
// build(url) → URL del proxy que envuelve la URL objetivo
// mode → 'raw' (devuelve el cuerpo tal cual) | 'json' (cuerpo en JSON)
// jsonField → si mode==='json', campo del que extraer el HTML
// needsEncode → si la URL objetivo debe ir percent-encoded
// ========================================
const ProxiesCORS = {
// ---- Arsenal (orden inicial; la salud lo reordena en caliente) ----
LISTA: [
// Familia AllOrigins — la más fiable en la práctica (raw y get/json).
{ id: 'allorigins-raw', build: u => `https://api.allorigins.win/raw?url=${encodeURIComponent(u)}`, mode: 'raw' },
{ id: 'allorigins-get', build: u => `https://api.allorigins.win/get?url=${encodeURIComponent(u)}`, mode: 'json', jsonField: 'contents' },
{ id: 'allorigins-hexlet', build: u => `https://allorigins.hexlet.app/raw?url=${encodeURIComponent(u)}`, mode: 'raw' },
{ id: 'allorigins-hx-get', build: u => `https://allorigins.hexlet.app/get?url=${encodeURIComponent(u)}`, mode: 'json', jsonField: 'contents' },
// codetabs — vivo, pero EXIGE la URL objetivo percent-encoded (antes daba 400).
{ id: 'codetabs', build: u => `https://api.codetabs.com/v1/proxy/?quest=${encodeURIComponent(u)}`, mode: 'raw' },
// Workers/Deno comunitarios que sí emiten cabeceras CORS.
{ id: 'whateverorigin', build: u => `https://whateverorigin.org/get?url=${encodeURIComponent(u)}`, mode: 'json', jsonField: 'contents' },
{ id: 'allorigins-cf', build: u => `https://api.allorigins.win/get?charset=UTF-8&url=${encodeURIComponent(u)}`, mode: 'json', jsonField: 'contents' }
],
// ---- Salud persistente (localStorage no está disponible en artifacts del
// chat, pero sí en el sitio desplegado; se degrada a memoria si falla) ----
_mem: {},
_CLAVE: 'statsim_proxy_health',
_cargarSalud() {
try {
const raw = (typeof localStorage !== 'undefined') && localStorage.getItem(this._CLAVE);
this._mem = raw ? JSON.parse(raw) : {};
} catch (e) { this._mem = {}; }
return this._mem;
},
_guardarSalud() {
try { if (typeof localStorage !== 'undefined') localStorage.setItem(this._CLAVE, JSON.stringify(this._mem)); }
catch (e) { /* memoria solamente */ }
},
// Puntaje: tasa de éxito reciente + bonus de velocidad − castigo por racha.
// Proxies probados y buenos se acercan a 1; los malos, a 0.
_score(id) {
const h = this._mem[id];
if (!h || (h.ok + h.fail) === 0) return 0.55; // sin historial: ligeramente sobre la media → se exploran pronto
const tasa = h.ok / (h.ok + h.fail);
const vel = h.msProm ? Math.max(0, 1 - h.msProm / 15000) : 0;
const castigo = Math.min(0.4, (h.rachaFail || 0) * 0.1);
return tasa * 0.7 + vel * 0.3 - castigo;
},
// Cuarentena TEMPORAL: un proxy con racha de fallos se aparta, pero se le
// da otra oportunidad pasado un tiempo (revive solo). Mejor que excluir
// para siempre, porque muchos proxies caen y vuelven.
_enCuarentena(id) {
const h = this._mem[id];
if (!h || (h.rachaFail || 0) < 4) return false;
const espera = Math.min(30, Math.pow(2, h.rachaFail - 4)) * 60000; // 1→…→30 min
return (Date.now() - (h.ultimoFail || 0)) < espera;
},
registrar(id, exito, ms) {
const h = this._mem[id] || (this._mem[id] = { ok: 0, fail: 0, msProm: 0, rachaFail: 0 });
if (exito) {
h.ok++; h.rachaFail = 0;
h.msProm = h.msProm ? Math.round(h.msProm * 0.7 + ms * 0.3) : ms;
} else { h.fail++; h.rachaFail = (h.rachaFail || 0) + 1; h.ultimoFail = Date.now(); }
this._guardarSalud();
},
// Lista ordenada por salud (mejor primero), excluyendo opcionalmente los
// que llevan demasiados fallos seguidos.
ordenados() {
if (!Object.keys(this._mem).length) this._cargarSalud();
const activos = this.LISTA.filter(p => !this._enCuarentena(p.id));
const pool = activos.length ? activos : this.LISTA; // si todos en cuarentena, reintenta todos
return pool
.map(p => ({ p, s: this._score(p.id) }))
.sort((a, b) => b.s - a.s)
.map(x => x.p);
},
// Extrae el HTML de la respuesta según el modo del proxy.
async extraer(proxy, resp) {
if (proxy.mode === 'json') {
const j = await resp.json();
return (proxy.jsonField ? j[proxy.jsonField] : j) || '';
}
return resp.text();
},
// ========================================================================
// CARRERA PARALELA: lanza los N mejores proxies a la vez contra el mismo
// objetivo y resuelve con el PRIMERO que entregue contenido válido (validar
// decide qué es "válido"). Cancela los demás. La latencia pasa de "suma de
// los que fallan" a "el más rápido que funciona". Registra salud de todos.
//
// objetivo : URL a pedir (ya con sus parámetros)
// validar : (htmlString) => obrasArray | null (null = respuesta inútil)
// op : { anchura=4, timeout=15000, oleadas=2 }
// Devuelve { obras, proxy } o lanza con diagnóstico.
// ========================================================================
async carrera(objetivo, validar, op = {}) {
if (typeof Promise.any !== 'function') return this._carreraSecuencial(objetivo, validar, op);
const anchura = op.anchura || 4;
const timeout = op.timeout || 15000;
const oleadas = op.oleadas || 2;
const cola = this.ordenados();
const diag = [];
for (let ola = 0; ola < oleadas && cola.length; ola++) {
const lote = cola.splice(0, anchura);
if (!lote.length) break;
// Un AbortController POR corredor, guardado en un array accesible
// desde fuera del .map() → así sí podemos cancelar a los perdedores.
const ctrls = lote.map(() => new AbortController());
const corredores = lote.map((proxy, i) => {
const t0 = Date.now();
const tid = setTimeout(() => ctrls[i].abort(), timeout);
// Cada corredor adjunta SU id de proxy al error (objeto, no string),
// para registrar salud por id real y nunca por el mensaje de fetch.
return fetch(proxy.build(objetivo), { signal: ctrls[i].signal })
.then(async r => {
clearTimeout(tid);
if (!r.ok) throw Object.assign(new Error(`HTTP${r.status}`), { proxyId: proxy.id });
const html = await this.extraer(proxy, r);
const obras = validar(html);
if (!obras || !obras.length) throw Object.assign(new Error('vacío'), { proxyId: proxy.id });
return { obras, proxy, ms: Date.now() - t0 };
})
.catch(e => { clearTimeout(tid); throw Object.assign(e instanceof Error ? e : new Error('err'), { proxyId: e && e.proxyId || proxy.id }); });
});
try {
const ganador = await Promise.any(corredores);
// CANCELACIÓN REAL de los rezagados (ahorra ancho de banda y, en
// Scholar, evita peticiones extra que dispararían el anti-bot).
ctrls.forEach((c, i) => { if (lote[i].id !== ganador.proxy.id) { try { c.abort(); } catch (e) {} } });
this.registrar(ganador.proxy.id, true, ganador.ms);
return { obras: ganador.obras, proxy: ganador.proxy.id, ms: ganador.ms };
} catch (agg) {
// Todos fallaron: registrar salud por proxyId REAL (no por mensaje).
const errs = (agg && agg.errors) ? agg.errors : [agg];
errs.forEach(e => {
if (e && e.proxyId) { this.registrar(e.proxyId, false); diag.push(`${e.proxyId}:${e.message}`); }
else diag.push(String(e && e.message || 'err'));
});
}
}
const err = new Error(diag.slice(0, 6).join(' · ') || 'ningún proxy respondió');
err.carrera = true;
throw err;
},
// Respaldo secuencial para navegadores sin Promise.any (ES2021).
async _carreraSecuencial(objetivo, validar, op = {}) {
const timeout = op.timeout || 15000;
const diag = [];
for (const proxy of this.ordenados().slice(0, (op.anchura || 4) * (op.oleadas || 2))) {
const t0 = Date.now();
const ctrl = new AbortController();
const tid = setTimeout(() => ctrl.abort(), timeout);
try {
const r = await fetch(proxy.build(objetivo), { signal: ctrl.signal });
clearTimeout(tid);
if (!r.ok) throw new Error('HTTP' + r.status);
const obras = validar(await this.extraer(proxy, r));
if (obras && obras.length) { this.registrar(proxy.id, true, Date.now() - t0); return { obras, proxy: proxy.id, ms: Date.now() - t0 }; }
this.registrar(proxy.id, false); diag.push(`${proxy.id}:vacío`);
} catch (e) { clearTimeout(tid); this.registrar(proxy.id, false); diag.push(`${proxy.id}:${e.message}`); }
}
const err = new Error(diag.slice(0, 6).join(' · ') || 'ningún proxy respondió'); err.carrera = true; throw err;
},
estado() {
return this.LISTA.map(p => ({ id: p.id, score: +this._score(p.id).toFixed(2), ...(this._mem[p.id] || {}) }));
}
};
if (typeof window !== 'undefined') {
window.ProxiesCORS = ProxiesCORS;
ProxiesCORS._cargarSalud();
}