

1/1/1970
Cómo conseguí que Foony funcionara detrás de proxies
¡Hola! Hace tiempo que sé que los proxies web causan problemas de compatibilidad para los sitios web. Sin embargo, el soporte de Foony en proxies ha sido notoriamente malo, y resolver la compatibilidad de Foony con proxies fue bastante complicado.
Tampoco es un caso de "Foony usa APIs exóticas" (aunque sí lo hacemos). Era una combinación de:
- Proxies que reescriben cadenas de forma agresiva en sitios donde no deberían hacerlo en absoluto.
- Proxies que tratan el dominio principal del sitio de forma distinta a "otros" dominios (CDNs, hosts de assets, etc.).
- Y la dura realidad de que algunos proxies simplemente no pueden soportar aplicaciones web modernas (corrección de HTTPS, WebSockets, etc.).
No funcionamos con todos los proxies, pero ahora funcionamos al menos con croxyproxy y proxyorb, que era el objetivo.
A continuación explico qué se rompió, por qué se rompió, y los arreglos que realmente importaron.
Intento 1: Shaders de Three.js válidos pero rotos
El síntoma
Cuando probé croxyproxy, no podía jugar a 8 Ball Pool ni a ninguno de los otros juegos de three.js de Foony. Seguía obteniendo un error de compilación de shaders en Three.js como:
- "Shader Error 1282 - VALIDATE_STATUS false"
Ese mensaje era casi totalmente inútil. Suele significar "tu shader no es válido, buena suerte". Genial. Si alguna vez te has preguntado por qué siempre uso mensajes de error únicos para cada error en Foony, esta es la razón. Ayuda a localizar problemas en lugar de simplemente decir "el código está roto, ve a arreglarlo".
¿Pero por qué estaban rompiéndose shaders de three.js perfectamente válidos? ¿Qué pasaba?
La causa real: proxies corrompiendo layout(location = N)
Three.js emite GLSL con calificadores de layout como:
layout(location = 0) in vec3 position;
Algunos proxies intentan reescribir cualquier cosa que parezca la API location de JavaScript haciendo un reemplazo global ingenuo de cadenas. Eso ya es malo en JS, pero también lo estaban haciendo dentro de las cadenas fuente de los shaders. Supongo que parsear el AST es demasiado caro para ellos.
Así que la fuente del shader quedaba corrompida en algo como:
layout(__cpLocation = 0) in vec3 position;
El identificador debe ser location ahí. Cualquier otra cosa es GLSL inválido, y el compilador lo rechaza. (Calificadores de Layout en GLSL)
Esto es un problema de Three.js solo en el sentido de que Three.js genera shaders dinámicamente, y los pasamos a WebGL en tiempo de ejecución. El error real está en la estrategia de reescritura del proxy.
Por qué no "arreglé el proxy"
Un enfoque ingenuo sería buscar la cadena de reemplazo de location de croxyproxy, __cpLocation, y reemplazarla por location. Sin embargo, distintos proxies usan distintos nombres de reemplazo. Algunos usan __cpLocation, otros usan otros identificadores raros. Así que codificar a fuego un arreglo como "reemplaza __cpLocation por location" es frágil.
Necesitaba:
- Un arreglo genérico (sin codificar a fuego identificadores de proxies).
- Un arreglo que funcionara incluso si el proxy también está reescribiendo la palabra
locationen mi JavaScript.
El truco del base64: ocultar la palabra location al proxy
Si el proxy reescribe cada location literal que ve, lo más sencillo es simplemente no usar location. Bastante fácil. Ya había visto trucos así en Lua cuando hice ingeniería inversa al sistema de protección de guías de RestedXP (si recuerdo bien, ofuscan su uso de BNGetInfo, por ejemplo _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Este truco también funciona en JavaScript, por supuesto. En client/index.html, decodifico lo siguiente en tiempo de ejecución:
// Como estos proxies intentan reemplazar cada `location`, usamos una cadena codificada en base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Ese atob() ocurre después de que el proxy ya haya hecho su reescritura del HTML/JS, así que no puede "pre-corromper" la cadena. Divido la cadena en dos para hacerla aún más difícil de detectar, y uso 'atob' porque puedo, pero String.fromCharCode o el escape hexadecimal window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] también podrían funcionar.
El patrón del shader roto siempre es estructuralmente el mismo:
layout(<algo> = <número>)
Así que lo busco de forma genérica y reemplazo <algo> con el identificador correcto:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
El hook de WebGL: parchear shaderSource (WebGL1 + WebGL2)
Como Three.js llama a gl.shaderSource(shader, source), parcheo shaderSource directamente:
const originalShaderSource = WebGLRenderingContext.prototype.shaderSource;
Object.defineProperty(WebGLRenderingContext.prototype, 'shaderSource', {
value: function (shader, source) {
return originalShaderSource.call(this, shader, fixCorruptedShaderSource(source));
},
writable: true,
configurable: true,
});
Y aplico el mismo parche a WebGL2RenderingContext si existe.
Una vez eso estuvo en su sitio, los errores de compilación de shaders desaparecieron. En este punto, croxyproxy funcionaba pero proxyorb seguía fallando. ¡¿Por qué?! ¿No debería funcionar igual?
Intento 2: El segundo problema (dominios) y por qué eliminar foony.io lo hizo todo más fácil
Foony históricamente usaba dos dominios, al menos durante el último mes:
foony.compara el sitio principalfoony.iopara los assets estáticos
La razón original era práctica: servir los assets desde un dominio sin cookies evita la sobrecarga de cabeceras de cookies en cada petición de archivo estático. Esto está genial, pero no es tan necesario como podrías pensar dado que HTTP/2 usa HPACK para reducir los bytes enviados por la red para las cabeceras.
Es una optimización válida en navegación normal.
Detrás de proxies, se convirtió en una fuente importante de roturas. Y a la base de usuarios de Foony le encantan los proxies. suspiro
Los proxies tratan "el sitio principal" de forma distinta a "otros sitios"
Muchos proxies están optimizados para "proxiar esta única página / dominio". Cargarán con éxito el HTML principal, inyectarán scripts, registrarán su propio ServiceWorker, etc.
Pero cuando la app empieza a tirar de assets desde un origen distinto (como foony.io), te metes en todo tipo de roturas divertidas:
- Fallos de interceptación del ServiceWorker como:
- "ServiceWorker intercepted the request and encountered an unexpected error"
- "Loading failed for the module with source"
- Parámetros de query que la infraestructura del proxy requiere (y que es fácil eliminar accidentalmente).
- Peticiones de assets perdiendo los metadatos de enrutamiento internos del proxy.
- Proxies raros que reemplazan toda la petición con
generic-php-slug.php?someQueryParam=hugeEncodedString(sí, no me molesté en soportar eso).
Los mecanismos internos de estos proxies dependen de los parámetros de query / codificación de URLs, y son bastante frágiles.
Uno de los ejemplos reveladores fue una URL de asset como:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Ese ?__pot=... es el propio enrutamiento/estado del proxy que le dice a qué dominio va dirigida la petición. Si lo eliminas, los proxies no pueden resolver la petición correctamente, y acabas en el camino del error del ServiceWorker.
El "intercambio de recursos" al rescate (y por qué se complicó rápidamente)
En un momento dado, probé una solución alternativa: detectar "estamos proxiados", luego intercambiar cualquier URL de recurso de foony.io al origen actual para que el proxy viera todo como del mismo origen.
Eso suena razonable, y funcionaba para croxyproxy, pero añadió mucha complejidad:
- Hay que reemplazar las etiquetas
linkyscriptque ya existen en el HTML. - Necesitas un
MutationObserverpara manejar etiquetas inyectadas dinámicamente (modulepreload, stylesheet, etc.). - Tienes que preservar los parámetros de query del proxy, o romperás su enrutamiento. Y distintos proxies hacen esto de forma distinta. Porque por supuesto.
- Y aun así tienes que mantener la lógica genérica (sin variables globales específicas del proxy) para que el código no se convierta en un basurero hinchado.
Aquí también volvió a aparecer el "truco del base64": incluso en mi propio JavaScript, tenía que tener cuidado con la cadena literal location porque el proxy podría reescribirla.
Ingeniería inversa del script inyectado de CroxyProxy
En este punto sentí curiosidad: ¿qué está haciendo realmente el proxy a mi página? ¿Está inyectando sus propios anuncios? ¿Algo peor?
El script del lado cliente de CroxyProxy está fuertemente ofuscado.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Lo cual al ejecutarse, da como resultado:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Basándome en esto, parece que croxyproxy está usando Obfuscator.io para esta ofuscación. Lo cual afortunadamente es lo bastante fácil de desofuscar con webcrack.
Esto da como resultado un JavaScript mucho más legible:
((_0x15ca2c, _0x489eaa) => {
if (typeof module == "object" && module.exports) {
module.exports = _0x489eaa(require("./punycode"), require("./IPv6"), require("./SecondLevelDomains"));
} else if (typeof define == "function" && define.amd) {
define(["./punycode", "./IPv6", "./SecondLevelDomains"], _0x489eaa);
} else {
_0x15ca2c.URI = _0x489eaa(_0x15ca2c.punycode, _0x15ca2c.IPv6, _0x15ca2c.SecondLevelDomains, _0x15ca2c);
}
})(this, function (_0x724467, _0x275183, _0x219d84, _0x2dcc2c) {
var _0x114c1e = _0x2dcc2c && _0x2dcc2c.URI;
function _0x5a9187(_0x2fd4ea, _0x3bd460) {
var _0x23a83f = arguments.length >= 1;
if (!(this instanceof _0x5a9187)) {
Bien. Ahora podemos ver lo que hace. Y... parece que mayormente está bien. Creo que la ofuscación es principalmente para ayudar a evitar la detección del proxy. Mayormente.
Tenemos algo de inyección de anuncios / UI:
Bi(_0x308e2f) {
console.log("Ads: " + _0x331b11.showAds);
console.log("Ad codes: " + !!_0x331b11.adsJson);
console.log("Adblock: " + _0x308e2f);
_0x34fa18.document.body.insertAdjacentHTML("afterbegin", _0x331b11.modal);
if (_0x331b11.header) {
if (_0x331b11.header) {
this.Ri(_0x308e2f);
}
[...document.querySelectorAll("#__cpsHeader a")].forEach(_0xcce595 => {
_0xcce595.addEventListener("click", function (_0x3ef5f1) {
if (this.target === "_blank") {
_0x34fa18.open(this.href, "_blank").focus();
} else {
_0x34fa18.location.href = this.href;
}
_0x3ef5f1.stopImmediatePropagation();
_0x3ef5f1.preventDefault();
}, true);
});
}
return this;
}
Hay otros sitios, pero básicamente está mostrando anuncios, incluidos anuncios pop-under. También usa FuckAdBlock.
El reemplazo real de las cadenas, sin embargo, está sucediendo en el lado del servidor. Y quién sabe qué estará haciendo todo eso.
En cualquier caso, no deberías usar proxies web bajo ningún concepto si te importa la seguridad de tu cuenta. Si tienes que hacerlo, evita introducir cualquier información personal (PII) / de cuenta / de compra.
El "intercambio de recursos" a la basura
Decidí que la complejidad del intercambio de recursos, sumada a la complejidad en otras partes del código para soportar foony.io, no merecía la pena por el pequeño ahorro de red de las preciosas peticiones sin cookies. También estábamos viendo una caída inexplicable en nuestras conversiones de juego desde que adoptamos foony.io, así que sospecho que había otros problemas con foony.io de los que no éramos conscientes.
Así que eliminé foony.io. Al menos por ahora.
Una vez borré la lógica del CDN de foony.io y estandaricé todo en foony.com, el soporte de proxies se volvió drásticamente más simple:
- Cargas de assets del mismo origen.
- Menos "casos especiales" que explicar al ServiceWorker de un proxy.
- Menos reescritura.
- Código menos frágil.
En resumen, eliminar foony.io fue una simplificación arquitectónica que redujo la superficie para comportamientos raros de proxies.
Intento 3: Qué funciona, qué no, y por qué
Proxies confirmados que funcionan
En este punto, Foony funciona detrás de:
- croxyproxy
- proxyorb
Probablemente algunos otros proxies funcionen. Apuesto a que la mayoría todavía no. Pero al menos los importantes que la gente usa para jugar parecen funcionar.
¿Por qué no "todos los proxies"?
Algunos proxies simplemente no pueden soportar una aplicación web multijugador moderna. Ejemplos:
- Proxies que no soportan HTTPS correctamente.
- Proxies que rompen o bloquean WebSockets (Foony usa redes en tiempo real). Técnicamente podrías sortearlo, pero añadiría complejidad.
- Proxies que tienen demasiadas restricciones en torno a peticiones cross-origin, cabeceras o ServiceWorkers.
Conclusiones clave
Los proxies web son muy inseguros
Son middleware que:
- reescribe HTML
- reescribe JavaScript
- a veces inyecta un ServiceWorker
- y a menudo depende de parámetros de query / codificación de URLs para enrutar peticiones
- puede manipular tus páginas de muchísimas formas
Me sorprendió cuán profundo llegan algunos proxies: reescribirán cadenas fuente de shaders, comentarios, y Dios sabe qué más.
A veces el mejor arreglo es arquitectónico
El parche de WebGL hizo que los juegos se renderizaran de nuevo, pero eliminar la estrategia de CDN multi-dominio hizo que el soporte de proxies se mantuviera estable.
Es un buen recordatorio: las optimizaciones ingeniosas pueden ser perfectamente razonables hasta que chocan con middleware hostil. O las extensiones del navegador del usuario. O Safari. O la configuración de idioma. O las funciones de accesibilidad. O las erupciones solares. O cualquier cosa, en realidad.
Conclusión
Foony ahora funciona detrás de los proxies que importan (croxyproxy y proxyorb), sin convertir el código base en un lío específico de proxies:
- Un arreglo genérico para los shaders de Three.js (sin identificadores específicos de proxies).
- Una estrategia de dominios más simple (foony.com en todas partes).