

1/1/1970
Hoe ik Foony achter proxy's heb laten werken
Hallo! Ik weet al lange tijd dat webproxy's compatibiliteitsproblemen veroorzaken voor websites. Foony's ondersteuning in proxy's was echter berucht slecht, en het oplossen van Foony's proxy-compatibiliteit was behoorlijk lastig.
Dit is ook geen "Foony gebruikt exotische API's"-probleem (ook al doen we dat). Het was een combinatie van:
- Proxy's die agressief strings herschrijven op plekken waar dat absoluut niet hoort.
- Proxy's die het hoofddomein van de site anders behandelen dan "andere" domeinen (CDN's, asset hosts, enzovoort).
- En de harde realiteit dat sommige proxy's gewoon geen moderne webapps kunnen ondersteunen (HTTPS-correctheid, WebSockets, enzovoort).
We werken niet met elke proxy, maar we werken nu in ieder geval met croxyproxy en proxyorb, en dat was het doel.
Hieronder leg ik uit wat er stuk ging, waarom het stuk ging, en welke fixes echt belangrijk waren.
Ronde 1: Geldige, kapotte Three.js-shaders
Het symptoom
Toen ik croxyproxy probeerde, kon ik 8 Ball Pool of een van Foony's andere three.js games niet spelen. Ik kreeg steeds een shader-compilatiefout in Three.js met meldingen zoals:
- "Shader Error 1282 - VALIDATE_STATUS false"
Die melding was vrijwel volledig nutteloos. Het betekent meestal "je shader is ongeldig, succes ermee." Geweldig. Als je je ooit afvraagt waarom ik altijd unieke foutmeldingen gebruik voor elke afzonderlijke fout op Foony, dit is waarom. Het helpt om problemen te lokaliseren in plaats van alleen "code is kapot, ga maar fixen."
Maar waarom braken perfect geldige three.js-shaders? Hoe zit dat?
De werkelijke oorzaak: proxy's die layout(location = N) corrumperen
Three.js genereert GLSL met layout-qualifiers zoals:
layout(location = 0) in vec3 position;
Sommige proxy's proberen alles wat lijkt op de JavaScript location-API te herschrijven door naïeve globale stringvervanging te doen. Dat is al slecht in JS, maar ze deden het ook binnen shader-bronstrings. Ik gok dat AST-parsing te duur voor ze is.
Dus de shader-broncode werd gecorrumpeerd tot iets als:
layout(__cpLocation = 0) in vec3 position;
De identifier moet daar location zijn. Iets anders is ongeldige GLSL, en de compiler weigert het. (Layout Qualifiers in GLSL)
Dit is alleen een Three.js-probleem in de zin dat Three.js dynamisch shaders genereert, en we ze tijdens runtime aan WebGL doorgeven. De echte bug is de herschrijfstrategie van de proxy.
Waarom ik "de proxy niet fixte"
Een naïeve aanpak zou zijn om te zoeken naar croxyproxy's location-vervangingsstring, __cpLocation, en die te vervangen door location. Verschillende proxy's gebruiken echter verschillende vervangingsnamen. Sommige gebruiken __cpLocation, andere gebruiken weer andere rare identifiers. Dus een fix zoals "vervang __cpLocation terug naar location" hardcoderen is fragiel.
Ik had nodig:
- Een generieke fix (geen hardcoded proxy-identifiers).
- Een fix die werkt zelfs als de proxy het woord
locationook in mijn JavaScript herschrijft.
De base64-truc: het woord location verbergen voor de proxy
Als de proxy elke letterlijke location die hij ziet herschrijft, is de simpelste zet om gewoon geen location te gebruiken. Eenvoudig genoeg. Ik heb eerder zulke trucs gezien in Lua toen ik RestedXP's guide-beveiligingssysteem reverse-engineerde (als ik me goed herinner verbergen ze hun gebruik van BNGetInfo, bijvoorbeeld _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Deze truc werkt natuurlijk ook in JavaScript. In client/index.html decodeer ik het volgende tijdens runtime:
// Omdat deze proxy's elke `location` proberen te vervangen, gebruiken we een base64-gecodeerde string.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Die atob() gebeurt nadat de proxy de HTML/JS-herschrijving al heeft gedaan, dus die kan de string niet "vooraf corrumperen." Ik splits de string in tweeën om het nog moeilijker detecteerbaar te maken, en ik gebruik 'atob' omdat het kan, maar String.fromCharCode of hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] zou ook kunnen werken.
Het kapotte shader-patroon is altijd structureel hetzelfde:
layout(<iets> = <getal>)
Dus matche ik dat generiek en vervang <iets> door de juiste identifier:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
De WebGL-hook: shaderSource patchen (WebGL1 + WebGL2)
Aangezien Three.js gl.shaderSource(shader, source) aanroept, patch ik shaderSource zelf:
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,
});
En ik pas dezelfde patch toe op WebGL2RenderingContext als die bestaat.
Toen dat eenmaal stond, verdwenen de shader-compilatiefouten. Op dit punt werkte croxyproxy, maar proxyorb faalde nog steeds. Waarom?! Zou dat niet hetzelfde moeten werken?
Ronde 2: Het tweede probleem (domeinen) en waarom het verwijderen van foony.io alles makkelijker maakte
Foony gebruikte historisch twee domeinen, in ieder geval de afgelopen maand:
foony.comvoor de hoofdsitefoony.iovoor statische assets
De oorspronkelijke reden was praktisch: het serveren van assets vanaf een cookie-loos domein voorkomt cookie-header-uploadbloat bij elk verzoek voor een statisch bestand. Dat is geweldig, maar niet zo noodzakelijk als je zou denken, gezien HTTP/2 HPACK gebruikt om het aantal bytes verzonden over de lijn voor headers te verminderen.
Het is een geldige optimalisatie bij normaal browsen.
Achter proxy's werd het een grote bron van breukschade. En Foony's gebruikersbasis is dol op proxy's. zucht
Proxy's behandelen "de hoofdsite" anders dan "andere sites"
Veel proxy's zijn geoptimaliseerd voor "proxy deze ene pagina / dit ene domein." Ze laden de hoofd-HTML succesvol, injecteren scripts, registreren hun eigen ServiceWorker, enzovoort.
Maar wanneer de app assets begint op te halen van een ander origin (zoals foony.io), kom je in allerlei leuke kapotheid terecht:
- ServiceWorker-onderscheppingsfouten zoals:
- "ServiceWorker intercepted the request and encountered an unexpected error"
- "Loading failed for the module with source"
- Queryparameters die vereist zijn door de proxy-infrastructuur (en gemakkelijk per ongeluk te strippen).
- Asset-verzoeken die de interne routeringsmetadata van de proxy verliezen.
- Rare proxy's die het hele verzoek vervangen door
generic-php-slug.php?someQueryParam=hugeEncodedString(ja, daar heb ik geen moeite voor gedaan om dat te ondersteunen).
Deze interne mechanismen van proxy's zijn afhankelijk van queryparameters / URL-encoding, en ze zijn behoorlijk fragiel.
Een van de duidelijke voorbeelden was een asset-URL als:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Die ?__pot=... is de eigen routerings-/state van de proxy die de proxy vertelt voor welk domein het verzoek is. Als je dat strip, kan de proxy het verzoek niet correct oplossen, en eindig je in het ServiceWorker-foutpad.
"Resource swapping" te hulp (en waarom het al snel ingewikkeld werd)
Op een bepaald moment probeerde ik een workaround: detecteer "we worden geproxied," en wissel dan alle foony.io-resource-URL's om naar het huidige origin, zodat de proxy alles als same-origin zou zien.
Dat klinkt redelijk, en het werkte voor croxyproxy, maar het voegde veel complexiteit toe:
- Je moet
link- enscript-tags vervangen die al in de HTML staan. - Je hebt een
MutationObservernodig om dynamisch geïnjecteerde tags af te handelen (modulepreload, stylesheet, enzovoort). - Je moet de queryparameters van de proxy behouden, anders breek je hun routering. En verschillende proxy's doen dit verschillend. Want natuurlijk doen ze dat.
- En je moet de logica nog steeds generiek houden (geen proxy-specifieke globals), zodat de code geen opgeblazen vuilnisbrand wordt.
Dit is ook waar de "base64-truc" weer naar voren kwam: zelfs in mijn eigen JavaScript moest ik voorzichtig zijn met de letterlijke string location, omdat de proxy die zou kunnen herschrijven.
CroxyProxy's geïnjecteerde script reverse-engineeren
Op dit punt werd ik nieuwsgierig: wat doet de proxy eigenlijk met mijn pagina? Injecteert hij eigen advertenties? Iets ergers?
CroxyProxy's client-side script is zwaar geobfusceerd.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Wat na uitvoeren resulteert in:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Op basis hiervan lijkt het erop dat croxyproxy Obfuscator.io gebruikt voor deze obfuscatie. Wat gelukkig makkelijk genoeg te de-obfusceren is met webcrack.
Dit resulteert in veel leesbaardere 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)) {
Mooi. Nu kunnen we zien wat het doet. En... het lijkt grotendeels in orde. Ik denk dat de obfuscatie er vooral is om detectie van de proxy te helpen voorkomen. Vooral.
We hebben wat advertentie- / UI-injectie:
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;
}
Er zijn nog wat andere plekken, maar in feite toont het gewoon advertenties, inclusief pop-under-achtige advertenties. Het gebruikt ook FuckAdBlock.
De echte vervanging van de strings gebeurt echter aan de serverkant. En wie weet wat dat allemaal doet.
Hoe dan ook, je zou absoluut geen webproxy's moeten gebruiken als je geeft om de veiligheid van je account. Als het moet, vermijd dan het invoeren van persoonlijke gegevens / accountinformatie / aankoopinformatie.
"Resource swapping" de prullenbak in
Ik besloot dat de complexiteit van resource swapping, gekoppeld aan de complexiteit in andere delen van de code voor foony.io-ondersteuning, het niet waard was voor de kleine netwerkbesparing van mooie, cookie-loze verzoeken. We zagen ook een onverklaarde daling in onze gameplay-conversies sinds we foony.io hadden ingevoerd, dus ik vermoed dat er andere problemen waren met foony.io waar we ons niet van bewust waren.
Dus heb ik foony.io verwijderd. In ieder geval voorlopig.
Nadat ik de foony.io CDN-logica had verwijderd en alles op foony.com had gestandaardiseerd, werd proxy-ondersteuning dramatisch eenvoudiger:
- Same-origin asset-loads.
- Minder "speciale gevallen" om uit te leggen aan een proxy-ServiceWorker.
- Minder herschrijving.
- Minder fragiele code.
Kort gezegd: het verwijderen van foony.io was een architecturale vereenvoudiging die het oppervlak voor raar proxy-gedrag verkleinde.
Ronde 3: Wat werkt, wat niet, en waarom
Bevestigd werkende proxy's
Op dit punt werkt Foony achter:
- croxyproxy
- proxyorb
Sommige andere proxy's werken waarschijnlijk ook. Ik wed dat de meeste het nog steeds niet doen. Maar in ieder geval lijken de belangrijke proxy's die mensen gebruiken om games op te spelen het wel te doen.
Waarom niet "alle proxy's"?
Sommige proxy's kunnen simpelweg geen moderne multiplayer-webapp ondersteunen. Voorbeelden:
- Proxy's die HTTPS niet goed ondersteunen.
- Proxy's die WebSockets breken of blokkeren (Foony gebruikt realtime networking). Technisch kun je hier omheen werken, maar dat zou complexiteit toevoegen.
- Proxy's die te veel beperkingen hebben rond cross-origin-verzoeken, headers of ServiceWorkers.
Belangrijkste lessen
Webproxy's zijn erg onveilig
Het is middleware die:
- HTML herschrijft
- JavaScript herschrijft
- soms een ServiceWorker injecteert
- en vaak afhankelijk is van queryparameters / URL-encoding om verzoeken te routeren
- op allerlei manieren met je pagina's kan rommelen
Ik was verrast hoe diep sommige proxy's gaan: ze herschrijven shader-bronstrings, commentaar en God weet wat nog meer.
Soms is de beste fix architecturaal
De WebGL-patch zorgde dat de games weer renderden, maar het verwijderen van de multi-domein CDN-strategie zorgde dat proxy-ondersteuning stabiel bleef.
Het is een goede herinnering: slimme optimalisaties kunnen perfect redelijk zijn totdat ze botsen met vijandige middleware. Of browserextensies van gebruikers. Of Safari. Of taalinstellingen. Of toegankelijkheidsfuncties. Of zonnevlammen. Of wat dan ook, eigenlijk.
Conclusie
Foony werkt nu achter de proxy's die ertoe doen (croxyproxy en proxyorb), zonder dat de codebase een proxy-specifieke chaos wordt:
- Een generieke Three.js-shaderfix (geen proxy-specifieke identifiers).
- Een eenvoudigere domeinstrategie (overal foony.com).