

1/1/1970
Come ho fatto funzionare Foony dietro i proxy
Ciao! So da un sacco di tempo che i proxy web causano problemi di compatibilità ai siti. Però il supporto di Foony nei proxy è sempre stato notoriamente pessimo, e sistemare la compatibilità di Foony con i proxy è stato piuttosto complicato.
E non è nemmeno un problema del tipo “Foony usa API esotiche” (anche se in effetti sì). È stata una combinazione di cose:
- Proxy che riscrivono le stringhe in modo aggressivo in punti in cui proprio non dovrebbero.
- Proxy che trattano il dominio principale del sito in modo diverso rispetto agli “altri” domini (CDN, host per asset, ecc.).
- E l’amara realtà che alcuni proxy non possono proprio supportare le web app moderne (correttezza HTTPS, WebSocket, ecc.).
Non funzioniamo con ogni singolo proxy, ma adesso almeno funzioniamo con croxyproxy e proxyorb, che era l’obiettivo.
Qui sotto spiego cosa si rompeva, perché succedeva e quali correzioni hanno fatto davvero la differenza.
Passaggio 1: shader Three.js validi ma rotti
Il sintomo
Quando ho provato croxyproxy, non riuscivo a giocare a 8 Ball Pool o agli altri giochi three.js di Foony qui. Continuavo a ricevere un errore di compilazione dello shader in Three.js, con errori tipo:
- “Shader Error 1282 - VALIDATE_STATUS false”
Quel messaggio era praticamente inutile. Di solito significa “il tuo shader è invalido, arrangiati”. Fantastico. Se ti sei mai chiesto perché su Foony uso sempre messaggi di errore unici per ogni singolo errore, è per questo. Aiuta a individuare i problemi invece di un generico “il codice è rotto, sistemalo”.
Ma perché shader three.js perfettamente validi si stavano rompendo? Che cosa stava succedendo?
La vera causa: i proxy corrompono layout(location = N)
Three.js genera GLSL con qualificatori layout del tipo:
layout(location = 0) in vec3 position;
Alcuni proxy provano a riscrivere qualunque cosa sembri l’API JavaScript location facendo una sostituzione di stringhe globale e ingenua. È già pessimo in JavaScript, ma lo facevano anche dentro le stringhe di sorgente degli shader. Immagino che fare il parsing dell’AST per loro sia troppo costoso.
Così il sorgente dello shader veniva corrotto in qualcosa tipo:
layout(__cpLocation = 0) in vec3 position;
L’identificatore lì deve essere location. Qualunque altra cosa è GLSL invalido, e il compilatore lo rifiuta. (Layout Qualifiers in GLSL)
È un problema di Three.js solo nel senso che Three.js genera gli shader in modo dinamico e noi li passiamo a WebGL a runtime. Il vero bug è la strategia di riscrittura del proxy.
Perché non ho “corretto il proxy”
Un approccio ingenuo sarebbe cercare la stringa di sostituzione di location usata da croxyproxy, __cpLocation, e rimpiazzarla con location. Però proxy diversi usano nomi di sostituzione diversi. Alcuni usano __cpLocation, altri usano identificatori ancora più strani. Quindi avere una correzione hardcoded tipo “rimpiazza __cpLocation con location” è fragile.
Mi serviva:
- Una correzione generica (niente identificatori specifici di un proxy).
- Una correzione che funzionasse anche se il proxy riscriveva la parola
locationanche nel mio JavaScript.
Il trucco della base64: nascondere la parola location al proxy
Se il proxy riscrive ogni location letterale che vede, la mossa più semplice è: non usare location. Facile a dirsi. Avevo già visto trucchetti del genere in Lua quando ho fatto il reverse engineering del sistema di protezione delle guide di RestedXP (se ricordo bene, offuscano l’uso di BNGetInfo, per esempio _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Questo trucco funziona anche in JavaScript, ovviamente. In client/index.html, decodifico questo a runtime:
// Because these proxies try to replace every `location`, we use a base64 encoded string.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Quella atob() avviene dopo che il proxy ha già fatto la sua riscrittura di HTML/JS, quindi non può “pre-corrompere” la stringa. Divido la stringa in due per renderla ancora più difficile da rilevare, e uso 'atob' perché posso, ma anche String.fromCharCode o l’escape esadecimale window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] potrebbero funzionare.
Il pattern dello shader rotto è sempre strutturalmente lo stesso:
layout(<something> = <number>)
Quindi lo individuo in modo generico e sostituisco <something> con l’identificatore corretto:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
L’hook WebGL: patch di shaderSource (WebGL1 + WebGL2)
Visto che Three.js chiama gl.shaderSource(shader, source), faccio la patch direttamente di 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 applico la stessa patch a WebGL2RenderingContext se esiste.
Una volta messa questa soluzione, gli errori di compilazione degli shader sono spariti. A questo punto croxyproxy funzionava, ma proxyorb continuava a fallire. Perché?! Non dovrebbe funzionare allo stesso modo?
Passaggio 2: il secondo problema (i domini) e perché togliere foony.io ha semplificato tutto
Storicamente Foony usava due domini, almeno nell’ultimo mese:
foony.comper il sito principalefoony.ioper gli asset statici
Il motivo originale era pratico: servire gli asset da un dominio senza cookie evita di caricare gli header dei cookie in ogni richiesta di file statico. È una cosa ottima, ma non così necessaria come si potrebbe pensare, visto che HTTP/2 usa HPACK per ridurre i byte inviati sulla rete per gli header.
È un’ottimizzazione valida nella navigazione normale.
Dietro ai proxy, è diventata una grossa fonte di problemi. E agli utenti di Foony i proxy piacciono un sacco. sigh
I proxy trattano “il sito principale” in modo diverso rispetto agli “altri siti”
Molti proxy sono ottimizzati per “proxyare questa pagina / dominio specifico”. Caricano correttamente l’HTML principale, iniettano gli script, registrano il loro ServiceWorker, ecc.
Ma quando l’app inizia a scaricare asset da un’origine diversa (tipo foony.io), comincia tutto un simpatico festival di rotture:
- Errori di intercettazione del ServiceWorker come:
- “ServiceWorker intercepted the request and encountered an unexpected error”
- “Loading failed for the module with source”
- Parametri di query richiesti dall’infrastruttura del proxy (e facilissimi da rimuovere per sbaglio).
- Richieste di asset che perdono i metadati di instradamento interni del proxy.
- Proxy strani che sostituiscono l’intera richiesta con
generic-php-slug.php?someQueryParam=hugeEncodedString(sì, non mi sono nemmeno sforzato di supportare quella roba).
Questi meccanismi interni dei proxy dipendono da parametri di query / codifica degli URL, e sono piuttosto fragili.
Uno degli esempi più evidenti era un URL di un asset del tipo:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Quel ?__pot=... è lo stato/routing interno del proxy, che gli dice per quale dominio è la richiesta. Se lo togli, i proxy non riescono più a risolvere la richiesta correttamente e finisci nel percorso di errore del ServiceWorker.
“Resource swapping” per salvarsi (e perché è diventato subito complicato)
A un certo punto ho provato una scappatoia: rilevare “siamo dietro un proxy”, e poi sostituire ogni URL di risorsa foony.io con l’origine corrente, così che il proxy vedesse tutto come same-origin.
A sentirla così sembra una buona idea, e infatti con croxyproxy funzionava, ma aggiungeva un sacco di complessità:
- Devi sostituire i tag
linkescriptche esistono già nell’HTML. - Devi usare un
MutationObserverper gestire i tag iniettati in modo dinamico (modulepreload, stylesheet, ecc.). - Devi conservare i parametri di query del proxy, altrimenti rompi il suo instradamento. E proxy diversi lo fanno in modo diverso, ovviamente.
- E devi comunque mantenere la logica generica (nessuna variabile globale specifica di un proxy), altrimenti il codice diventa un pattume ingestibile.
Qui è tornato fuori anche il “trucco della base64”: perfino nel mio JavaScript dovevo stare attento alla stringa letterale location, perché il proxy poteva riscriverla.
Fare reverse engineering dello script iniettato da CroxyProxy
A quel punto mi sono incuriosito: ma che cosa fa davvero il proxy alla mia pagina? Inietta pubblicità sua? Qualcosa di peggio?
Lo script lato client di CroxyProxy è pesantemente offuscato.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Che, eseguito, diventa:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Da questo sembra che croxyproxy usi Obfuscator.io per l’offuscamento. Che per fortuna è abbastanza semplice da deoffuscare con webcrack.
Il risultato è JavaScript molto più leggibile:
((_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)) {
Bene. Ora possiamo vedere cosa fa. E... sembra tutto sommato ok. Secondo me l’offuscamento serve soprattutto a rendere più difficile il rilevamento del proxy. Perlopiù.
C’è un po’ di iniezione di pubblicità / 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;
}
Ci sono altri punti simili, ma in pratica mostra pubblicità, anche in stile pop-under. Usa anche FuckAdBlock.
La vera sostituzione delle stringhe, però, avviene lato server. E chi sa che cos’altro stia facendo lì.
In ogni caso, non dovresti assolutamente usare proxy web se tieni alla sicurezza del tuo account. Se proprio devi, evita di inserire qualunque tuo dato personale / account / informazione di pagamento.
“Resource swapping” nel cestino
Ho deciso che la complessità del resource swapping, sommata alla complessità nelle altre parti del codice per supportare foony.io, non valeva il piccolo risparmio di rete delle belle richieste senza cookie. Stavamo anche vedendo un calo inspiegabile nelle conversioni di gioco da quando avevamo adottato foony.io, quindi sospetto che ci fossero altri problemi con foony.io di cui non eravamo nemmeno a conoscenza.
Quindi ho rimosso foony.io. Almeno per ora.
Una volta eliminata la logica CDN di foony.io e standardizzato tutto su foony.com, il supporto ai proxy è diventato molto più semplice:
- Caricamenti degli asset same-origin.
- Meno “casi speciali” da spiegare al ServiceWorker del proxy.
- Meno riscritture.
- Codice meno fragile.
In breve, togliere foony.io è stata una semplificazione architetturale che ha ridotto la superficie esposta ai comportamenti strani dei proxy.
Passaggio 3: cosa funziona, cosa no e perché
Proxy che funzionano confermati
A questo punto Foony funziona dietro:
- croxyproxy
- proxyorb
Probabilmente anche altri proxy funzionano. Scommetto che la maggior parte ancora no. Ma almeno quelli importanti che la gente usa per giocare sembrano andare.
Perché non “tutti i proxy”?
Alcuni proxy proprio non riescono a supportare una web app multiplayer moderna. Per esempio:
- Proxy che non supportano correttamente HTTPS.
- Proxy che rompono o bloccano i WebSocket (Foony usa networking in tempo reale). Tecnicamente potresti aggirare il problema, ma aggiungerebbe parecchia complessità.
- Proxy che hanno troppe restrizioni sulle richieste cross-origin, sugli header o sui ServiceWorker.
Cose importanti da ricordare
I proxy web sono molto insicuri
Sono middleware che:
- riscrivono l’HTML
- riscrivono il JavaScript
- a volte iniettano un ServiceWorker
- e spesso si basano su parametri di query / codifica degli URL per instradare le richieste
- possono giocherellare con le tue pagine in un’infinità di modi
Mi ha sorpreso quanto a fondo arrivino alcuni proxy: riscrivono perfino le stringhe di sorgente degli shader, i commenti e chissà cos’altro.
A volte la soluzione migliore è architetturale
La patch per WebGL ha fatto tornare a renderizzare i giochi, ma la rimozione della strategia CDN multi-dominio ha reso il supporto ai proxy stabile nel tempo.
È un buon promemoria: le ottimizzazioni “furbe” possono essere perfettamente ragionevoli finché non si scontrano con middleware ostili. O con le estensioni del browser degli utenti. O con Safari. O con le impostazioni di lingua. O con le funzioni di accessibilità. O con le tempeste solari. O con qualunque altra cosa, davvero.
Conclusione
Ora Foony funziona dietro i proxy che contano (croxyproxy e proxyorb), senza trasformare il codice in un pasticcio pieno di eccezioni per ogni proxy:
- Una correzione generica per gli shader di Three.js (niente identificatori specifici di un proxy).
- Una strategia di domini più semplice (foony.com ovunque).