

1/1/1970
Como eu fiz Foony funcionar atrás de proxies
Oi! Já faz tempo que eu sei que proxies web causam problemas de compatibilidade para sites. Mas o suporte de Foony em proxies sempre foi notoriamente ruim, e resolver a compatibilidade de Foony com proxies foi bem trabalhoso.
E isso nem é um caso de “Foony usa APIs exóticas” (apesar de a gente usar). Foi uma combinação de:
- Proxies fazendo substituição agressiva de strings em lugares onde não deveriam mexer.
- Proxies tratando o domínio principal do site de um jeito diferente dos “outros” domínios (CDNs, hosts de assets etc.).
- E a dura realidade de que alguns proxies simplesmente não conseguem lidar com apps web modernos (correção de HTTPS, WebSockets etc.).
A gente não funciona em todo proxy, mas agora funciona pelo menos no croxyproxy e no proxyorb, que era a meta.
Abaixo eu explico o que quebrou, por que quebrou e quais foram as correções que realmente importaram.
Passo 1: Shaders do Three.js válidos, porém quebrados
O sintoma
Quando eu testei o croxyproxy, eu não conseguia jogar 8 Ball Pool nem nenhum dos outros jogos de three.js de Foony. Eu só via falha de compilação de shader no Three.js, com erros tipo:
- “Shader Error 1282 - VALIDATE_STATUS false”
Essa mensagem era praticamente inútil. Normalmente quer dizer “seu shader é inválido, boa sorte”. Maravilha. Se um dia você se perguntar por que eu sempre coloco mensagens de erro únicas para cada erro em Foony, é por isso. Ajuda a apontar o problema em vez de só dizer “deu erro, se vira aí”.
Mas por que shaders de three.js perfeitamente válidos estavam quebrando? O que estava acontecendo?
A causa real: proxies corrompendo layout(location = N)
Three.js gera GLSL com qualifiers de layout assim:
layout(location = 0) in vec3 position;
Alguns proxies tentam reescrever qualquer coisa que pareça com a API location do JavaScript, fazendo uma substituição global de string bem ingênua. Isso já é ruim em JS, mas eles também faziam isso dentro das strings do código dos shaders. Imagino que fazer parsing de AST seja caro demais pra eles.
Então o código do shader era corrompido e virava algo assim:
layout(__cpLocation = 0) in vec3 position;
O identificador ali precisa ser location. Qualquer outra coisa é GLSL inválido, e o compilador rejeita. (Qualificadores de layout em GLSL)
Isso é um problema do Three.js só no sentido de que ele gera shaders dinamicamente, e a gente passa esses shaders para o WebGL em tempo de execução. O bug de verdade está na estratégia de reescrita do proxy.
Por que eu não “corrigi o proxy”
Uma abordagem ingênua seria procurar a string de substituição de location do croxyproxy, __cpLocation, e trocar de volta para location. Só que proxies diferentes usam nomes de substituição diferentes. Alguns usam __cpLocation, outros usam identificadores esquisitos de outro tipo. Então colocar um remendo fixo tipo “substituir __cpLocation de volta por location” é bem frágil.
Eu precisava de:
- Uma correção genérica (sem enfiar identificadores específicos de cada proxy).
- Uma correção que funcionasse mesmo se o proxy estivesse reescrevendo a palavra
locationtambém no meu JavaScript.
O truque do base64: escondendo a palavra location do proxy
Se o proxy reescreve todo location literal que vê, o jeito mais simples é simplesmente não usar location. Fácil, né. Eu já tinha visto truques parecidos em Lua quando eu fiz engenharia reversa do sistema de proteção de guias do RestedXP (se eu lembro direito, eles ofuscam o uso de BNGetInfo, tipo _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Esse truque funciona em JavaScript também, claro. No client/index.html, eu decodifico o seguinte em tempo de execução:
// Como esses proxies tentam substituir todo `location`, usamos uma string em base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Esse atob() roda depois de o proxy já ter feito a reescrita de HTML/JS, então ele não consegue “pré-corromper” a string. Eu divido a string em duas partes para ficar ainda mais difícil de detectar, e uso 'atob' porque sim, mas String.fromCharCode ou fazer escape em hexa de window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] também funcionariam.
O padrão do shader quebrado é sempre estruturalmente o mesmo:
layout(<algo> = <número>)
Então eu faço um match genérico e troco esse <algo> pelo identificador certo:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
O gancho no WebGL: modificando shaderSource (WebGL1 + WebGL2)
Como o Three.js chama gl.shaderSource(shader, source), eu sobrescrevo o próprio shaderSource:
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,
});
E eu aplico o mesmo patch em WebGL2RenderingContext se ele existir.
Depois disso, os erros de compilação de shader sumiram. Nesse ponto, o croxyproxy já funcionava, mas o proxyorb ainda falhava. Por quê?! Não era pra funcionar do mesmo jeito?
Passo 2: O segundo problema (domínios) e por que remover foony.io deixou tudo mais fácil
Historicamente, Foony usava dois domínios, pelo menos no último mês:
foony.compara o site principalfoony.iopara assets estáticos
O motivo original era prático: servir assets de um domínio sem cookies evita o inchaço de cabeçalhos de cookie em toda requisição de arquivo estático. Isso é ótimo, mas não é tão necessário quanto parece, já que HTTP/2 usa HPACK para reduzir os bytes de cabeçalhos enviados pela rede.
É uma otimização válida em navegação normal.
Por trás de proxies, isso virou uma baita fonte de quebra. E a base de jogadores de Foony ama proxies. suspiro
Proxies tratam “o site principal” de um jeito e “outros sites” de outro
Muitos proxies são otimizados para “proxyar esta página / este domínio aqui”. Eles carregam o HTML principal, injetam scripts, registram o próprio ServiceWorker etc.
Mas quando o app começa a buscar assets de outra origem (tipo foony.io), você cai em todo tipo de quebradeira divertida:
- Falhas de interceptação do ServiceWorker como:
- “ServiceWorker intercepted the request and encountered an unexpected error”
- “Loading failed for the module with source”
- Parâmetros de query exigidos pela infraestrutura do proxy (e fáceis de remover sem querer).
- Requisições de asset perdendo os metadados internos de roteamento do proxy.
- Proxies esquisitos que substituem a requisição inteira por
generic-php-slug.php?someQueryParam=hugeEncodedString(é, eu não fiz questão de dar suporte pra isso).
Os mecanismos internos desses proxies dependem de parâmetros de query / codificação de URL, e são bem frágeis.
Um dos exemplos mais óbvios era uma URL de asset tipo:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Esse ?__pot=... é o estado/roteamento próprio do proxy, que diz pra qual domínio aquela requisição é. Se você remove isso, o proxy não consegue resolver a requisição direito, e você cai em caminhos de erro do ServiceWorker.
“Troca de recursos” para salvar (e por que isso complicou tudo rapidinho)
Em certo momento, eu tentei um contorno: detectar “estamos atrás de um proxy” e então trocar qualquer URL de recurso foony.io para a origem atual, para o proxy ver tudo como same-origin.
Na teoria parece razoável, e funcionou para o croxyproxy, mas adicionou muita complexidade:
- Você precisa substituir tags
linkescriptque já existem no HTML. - Você precisa de um
MutationObserverpara lidar com tags injetadas dinamicamente (modulepreload, stylesheet etc.). - Você precisa preservar os parâmetros de query do proxy, ou quebra o roteamento dele. E cada proxy faz isso de um jeito. Porque claro que fazem.
- E ainda precisa manter a lógica genérica (sem globais específicas de proxy) para o código não virar um monstrozinho inchado de remendos.
Foi aqui também que o “truque do base64” voltou à tona: até no meu próprio JavaScript eu tinha que tomar cuidado com a string literal location, porque o proxy podia reescrevê-la.
Fazendo engenharia reversa do script injetado pelo CroxyProxy
Nessa hora bateu a curiosidade: o que o proxy está fazendo de verdade com a minha página? Está injetando anúncios dele? Algo pior?
O script client-side do CroxyProxy é bem ofuscado.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Que, quando executado, vira:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Pelo que dá pra ver, parece que o croxyproxy está usando o Obfuscator.io para fazer essa ofuscação. E felizmente é simples o bastante de desofuscar com o webcrack.
Isso gera um JavaScript bem mais legível:
((_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)) {
Legal. Agora dá pra ver o que está acontecendo. E... parece basicamente ok. Acho que a ofuscação é mais para dificultar a detecção do proxy. Mais ou menos.
Temos um pouco de injeção de anúncio / 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;
}
Tem outros trechos, mas basicamente é só exibir anúncios, incluindo anúncios no estilo pop-under. Ele também usa o FuckAdBlock.
A reescrita de strings de verdade, porém, acontece do lado do servidor. E aí quem sabe o que mais está rolando lá.
De qualquer forma, você definitivamente não deveria usar proxies web se se importa com a segurança da sua conta. Se for usar, evite digitar qualquer dado pessoal (PII) / de conta / de compra.
Jogando a “troca de recursos” no lixo
Eu decidi que a complexidade trazida pela troca de recursos, somada à complexidade em outras partes do código para suportar foony.io, não valia a pequena economia de rede de ter requisições lindas e sem cookies. A gente também viu uma queda estranha nas conversões de gameplay depois de adotar foony.io, então eu desconfio que tinham outros problemas com foony.io que a gente nem tinha percebido.
Então eu removi o foony.io. Pelo menos por enquanto.
Depois que eu deletei a lógica de CDN do foony.io e padronizei tudo em foony.com, o suporte a proxies ficou muito mais simples:
- Carregamento de assets em mesma origem.
- Menos “casos especiais” para explicar para o ServiceWorker do proxy.
- Menos reescrita.
- Código menos frágil.
Resumindo, remover o foony.io foi uma simplificação de arquitetura que reduziu a área de superfície para comportamento esquisito de proxies.
Passo 3: O que funciona, o que não funciona e por quê
Proxies confirmados funcionando
Hoje, Foony funciona atrás de:
- croxyproxy
- proxyorb
Provavelmente alguns outros proxies também funcionam. Aposto que a maioria ainda não. Mas pelo menos os mais importantes, que o pessoal usa pra jogar, parecem funcionar.
Por que não “todos os proxies”?
Alguns proxies simplesmente não conseguem dar conta de um app web multiplayer moderno. Por exemplo:
- Proxies que não suportam HTTPS direito.
- Proxies que quebram ou bloqueiam WebSockets (Foony usa rede em tempo real). Tecnicamente dá pra contornar isso, mas adicionaria mais complexidade.
- Proxies com muitas restrições em cima de requisições cross-origin, cabeçalhos ou ServiceWorkers.
Principais aprendizados
Proxies web são muito inseguros
Eles são um middleware que:
- reescreve HTML
- reescreve JavaScript
- às vezes injeta um ServiceWorker
- muitas vezes depende de parâmetros de query / codificação de URL para rotear requisições
- pode mexer nas suas páginas de um monte de jeitos diferentes
Eu me surpreendi com o quão fundo alguns proxies vão: eles reescrevem strings de código de shader, comentários e sabe-se lá mais o quê.
Às vezes a melhor correção é de arquitetura
O patch do WebGL fez os jogos voltarem a renderizar, mas remover a estratégia de CDN multi-domínio fez o suporte a proxy continuar estável.
É um bom lembrete: otimizações espertas podem ser totalmente razoáveis até baterem de frente com um middleware hostil. Ou com extensões do navegador do usuário. Ou com o Safari. Ou com configurações de idioma. Ou com recursos de acessibilidade. Ou com explosões solares. Ou com qualquer coisa, na real.
Conclusão
Agora Foony funciona atrás dos proxies que importam (croxyproxy e proxyorb), sem transformar o código em um caos cheio de casos específicos para proxy:
- Uma correção genérica para shaders do Three.js (sem identificadores específicos de proxy).
- Uma estratégia de domínio mais simples (foony.com em todo lugar).