

1/1/1970
How I Made Foony Work Behind Proxies
Hejsa! Jeg har i lang tid vidst, at webproxyer kan give kompatibilitetsproblemer for websites. Men Foony har været notorisk dårlig til at fungere bag proxyer, og det var ret bøvlet at få Foonys proxy-kompatibilitet på plads.
Det er heller ikke et “Foony bruger en masse eksotiske API'er”-problem (selvom det gør vi). Det var en kombination af:
- Proxyer, der omskriver tekststrenge helt vildt steder, hvor de slet ikke burde røre noget.
- Proxyer, der behandler det primære domæne anderledes end “andre” domæner (CDN'er, asset-hosts osv.).
- Og den hårde sandhed, at nogle proxyer bare ikke kan håndtere moderne webapps (korrekt HTTPS, WebSockets osv.).
Foony virker ikke bag alle proxyer, men den virker nu i hvert fald bag croxyproxy og proxyorb, og det var målet.
Herunder forklarer jeg, hvad der gik i stykker, hvorfor det skete, og hvilke løsninger der faktisk gjorde en forskel.
Første forsøg: Gyldige, men ødelagte Three.js-shadere
Symptomet
Da jeg prøvede croxyproxy, kunne jeg ikke spille 8 Ball Pool eller nogen af Foonys andre three.js spil. Jeg fik hele tiden en shader-kompilationsfejl i Three.js med fejl som:
- “Shader Error 1282 - VALIDATE_STATUS false”
Den besked var næsten totalt ubrugelig. Den betyder som regel noget i stil med “din shader er ugyldig, held og lykke.” Fedt. Hvis du nogensinde har undret dig over, hvorfor jeg altid bruger unikke fejlbeskeder til hver eneste fejl i Foony, så er det derfor. Det gør det meget nemmere at pege direkte på problemet i stedet for bare "koden er i stykker, gå fix det".
Men hvorfor gik helt gyldige Three.js-shadere i stykker? Hvad foregik der?
Den egentlige årsag: proxyer, der ødelægger layout(location = N)
Three.js genererer GLSL med layout-qualifiers som:
layout(location = 0) in vec3 position;
Nogle proxyer prøver at omskrive alt, der ligner JavaScripts location-API, ved at lave en naiv global tekstudskiftning. Det er allerede slemt i JS, men de gjorde det også inde i selve shader-kildekoden. Jeg gætter på, at rigtig AST-parsing er for dyrt for dem.
Så shader-kilden blev ødelagt og endte med noget i stil med:
layout(__cpLocation = 0) in vec3 position;
Identifikatoren skal være location der. Alt andet er ugyldig GLSL, og compileren afviser det. (Layout Qualifiers in GLSL)
Det er kun et Three.js-problem i den forstand, at Three.js genererer shadere dynamisk, og vi sender dem videre til WebGL ved runtime. Den rigtige fejl ligger i proxyens måde at omskrive ting på.
Hvorfor jeg ikke “fikset proxyen”
En naiv løsning ville være at lede efter croxyproxys location-erstatningsstreng, __cpLocation, og bytte den tilbage til location. Men forskellige proxyer bruger forskellige erstatningsnavne. Nogle bruger __cpLocation, andre bruger helt andre mærkelige navne. Så at hardcode en løsning som “erstat __cpLocation med location” er ret skrøbeligt.
Jeg havde brug for:
- En generel løsning (uden hardcodede proxy-navne).
- En løsning, der virker, selv hvis proxyen også omskriver selve ordet
locationi min JavaScript.
Base64-tricket: gemme ordet location for proxyen
Hvis proxyen omskriver hvert eneste location, den ser, er det nemmeste bare ikke at skrive location nogen steder. Fair nok. Jeg har set tricks til det her før i Lua, da jeg reverse-engineerede RestedXPs guide-beskyttelsessystem (hvis jeg husker rigtigt, slører de deres brug af BNGetInfo, fx _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Det trick virker selvfølgelig også i JavaScript. I client/index.html dekoder jeg det her ved 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
Det atob() bliver kaldt efter, at proxyen allerede har omskrevet HTML/JS, så den kan ikke “for-ødelægge” strengen. Jeg splitter strengen i to for at gøre den endnu sværere at opdage, og jeg bruger 'atob', bare fordi jeg kan, men String.fromCharCode eller hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] ville sikkert også virke.
Det ødelagte shader-mønster ser altid strukturelt ens ud:
layout(<something> = <number>)
Så jeg matcher det generelt og erstatter <something> med den rigtige identifikator:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
WebGL-hooket: patch shaderSource (WebGL1 + WebGL2)
Da Three.js kalder gl.shaderSource(shader, source), patcher jeg selve 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,
});
Og jeg laver den samme patch på WebGL2RenderingContext, hvis den findes.
Da det først var på plads, forsvandt shader-kompilationsfejlene. På det tidspunkt virkede croxyproxy, men proxyorb fejlede stadig. Hvorfor?! Burde det ikke fungere på samme måde?
Andet forsøg: Det næste problem (domæner) og hvorfor det blev meget nemmere at fjerne foony.io
Foony har historisk brugt to domæner, i hvert fald den sidste måneds tid:
foony.comtil selve hovedsidenfoony.iotil statiske filer
Den oprindelige grund var praktisk: når assets kommer fra et domæne uden cookies, slipper man for, at hver eneste statiske filanmodning slæber et stort cookie-header med sig. Det er fedt, men ikke helt så nødvendigt, som man skulle tro, når nu HTTP/2 bruger HPACK til at reducere antallet af header-bytes på linjen.
Det er en fin optimering i almindelig browsing.
Bag proxyer blev det en stor kilde til fejl. Og Foonys brugere elsker proxyer. suk
Proxyer behandler “hovedsiden” anderledes end “andre sider”
Mange proxyer er optimeret til “proxy den her ene side / det her ene domæne”. De indlæser fint hoved-HTML'en, injicerer scripts, registrerer deres egen ServiceWorker osv.
Men når appen begynder at hente assets fra et andet origin (som foony.io), kommer man ud i alle mulige sjove former for breakdowns:
- ServiceWorker-fejl som:
- “ServiceWorker intercepted the request and encountered an unexpected error”
- “Loading failed for the module with source”
- Query-parametre, som proxyens egen infrastruktur kræver (og som er nemme at komme til at fjerne ved et uheld).
- Asset-forespørgsler, der mister proxyens interne routing-metadata.
- Mærkelige proxyer, der erstatter hele requestet med
generic-php-slug.php?someQueryParam=hugeEncodedString(ja, det gad jeg ikke engang prøve at understøtte).
Proxyernes interne mekanismer er afhængige af query-parametre og URL-encoding, og de er ret skrøbelige.
Et af de tydelige eksempler var en asset-URL som:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Det ?__pot=... er proxyens egen routing/state, som fortæller proxyen, hvilket domæne anmodningen egentlig er til. Hvis du fjerner det, kan proxyen ikke løse anmodningen korrekt, og du ender i ServiceWorker-fejlvejen.
“Resource swapping” til undsætning (og hvorfor det hurtigt blev kompliceret)
På et tidspunkt prøvede jeg en workaround: opdage “vi er bag en proxy” og så erstatte alle foony.io-resource-URL'er med det nuværende origin, så proxyen ville se det hele som same-origin.
Det lyder meget fornuftigt, og det virkede for croxyproxy, men det tilføjede en masse kompleksitet:
- Man skal erstatte
link- ogscript-tags, der allerede ligger i HTML'en. - Man skal bruge en
MutationObservertil at håndtere tags, der bliver indsat dynamisk (modulepreload, stylesheets osv.). - Man skal bevare proxyens query-parametre, ellers ødelægger man deres routing. Og forskellige proxyer gør det på hver deres måde. Selvfølgelig gør de det.
- Og man skal stadig holde logikken generisk (ingen proxy-specifikke globale variabler), så koden ikke ender som et tungt, brændende skraldeskur.
Det var også her, “base64-tricket” dukkede op igen: selv i min egen JavaScript var jeg nødt til at passe på med selve strengen location, fordi proxyen kunne finde på at omskrive den.
Reverse-engineering af CroxyProxys injicerede script
På det her tidspunkt blev jeg nysgerrig: hvad gør proxyen egentlig ved min side? Indsætter den sine egne reklamer? Noget værre?
CroxyProxys client-side-script er kraftigt obfuskeret.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Når man kører det, ender man med:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Ud fra det ser det ud til, at croxyproxy bruger Obfuscator.io til obfuskeringen. Heldigvis er det ret nemt at deobfuskere med webcrack.
Det giver meget mere læsbar JavaScript:
((_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)) {
Fedt. Nu kan vi se, hvad det laver. Og... det ser mestendels okay ud. Jeg tror mest, obfuskeringen skal gøre det sværere at opdage, at der er en proxy indblandet. Mest.
Der er lidt annonce- / UI-injektion:
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;
}
Der er lidt flere steder rundt omkring, men grundlæggende handler det bare om at vise reklamer, inklusive pop-under-agtige annoncer. Det bruger også FuckAdBlock.
Den rigtige udskiftning af strenge sker dog på serversiden. Og hvem ved, hvad alt det ellers laver.
Uanset hvad bør du helt klart ikke bruge webproxyer, hvis du går op i sikkerheden på dine konti. Hvis du absolut skal, så lad være med at skrive nogen som helst personlige oplysninger, kontodetaljer eller betalingsinfo ind.
"Resource swapping" i skraldespanden
Jeg besluttede, at kompleksiteten fra resource swapping, kombineret med alt det ekstra kode-rod for at understøtte foony.io, ikke var det værd i forhold til de få netværks-besparelser ved nogle lidt pænere, cookieløse requests. Vi så også et uforklarligt fald i, hvor mange der faktisk begyndte at spille, efter vi tog foony.io i brug, så jeg mistænker, at der har været andre problemer med foony.io, vi ikke kendte til.
Så jeg fjernede foony.io. I hvert fald for nu.
Da jeg fjernede al CDN-logikken omkring foony.io og standardiserede alt på foony.com, blev proxy-understøttelsen markant simplere:
- Assets bliver hentet som same-origin.
- Færre “special cases” at forklare en proxy-ServiceWorker.
- Mindre omskrivning.
- Mindre skrøbelig kode.
Kort sagt var det en arkitektonisk forenkling at fjerne foony.io, som reducerede mængden af mærkelig proxy-adfærd, vi skulle tage højde for.
Tredje forsøg: Hvad der virker, hvad der ikke gør, og hvorfor
Proxyer, der bekræftet virker
På nuværende tidspunkt virker Foony bag:
- croxyproxy
- proxyorb
Nogle andre proxyer virker sikkert også. Jeg vil gætte på, at de fleste stadig ikke gør. Men de vigtige, som folk faktisk bruger til at spille på, ser i det mindste ud til at virke.
Hvorfor ikke “alle proxyer”?
Nogle proxyer kan simpelthen ikke understøtte en moderne multiplayer-webapp. For eksempel:
- Proxyer, der ikke understøtter HTTPS ordentligt.
- Proxyer, der ødelægger eller blokerer WebSockets (Foony bruger real-time netværk). Teknisk set kunne man arbejde sig uden om det, men det ville gøre alting mere kompliceret.
- Proxyer med alt for mange begrænsninger omkring cross-origin-forespørgsler, headers eller ServiceWorkers.
Hovedpointer
Webproxyer er meget usikre
De er et stykke middleware, der:
- omskriver HTML
- omskriver JavaScript
- nogle gange injicerer en ServiceWorker
- ofte er afhængig af query-parametre / URL-encoding for at kunne rute forespørgsler
- kan pille ved dine sider på alle mulige måder
Jeg blev faktisk overrasket over, hvor dybt nogle proxyer går: de omskriver shader-kildestrenge, kommentarer og Gud ved hvad ellers.
Nogle gange er den bedste løsning arkitektonisk
WebGL-patchen fik spillene til at tegne igen, men det var først, da vi droppede multi-domæne-CDN-strategien, at proxy-understøttelsen blev ved med at være stabil.
Det er en god påmindelse: smarte optimeringer kan være helt fornuftige, indtil de støder ind i fjendtligt middleware. Eller brugernes browser-udvidelser. Eller Safari. Eller sprokindstillinger. Eller tilgængelighedsfunktioner. Eller solstorme. Eller hvad som helst, egentlig.
Konklusion
Foony virker nu bag de proxyer, der betyder noget (croxyproxy og proxyorb), uden at kodebasen er blevet til et proxy-specifikt rod:
- En generel Three.js-shader-løsning (ingen proxy-specifikke identifikatorer).
- En simplere domænestrategi (foony.com overalt).