

1/1/1970
Hur jag fick Foony att fungera bakom proxyservrar
Hejsan! Jag har länge vetat att webbproxyservrar orsakar kompatibilitetsproblem för webbplatser. Men Foonys stöd i proxyservrar har varit ökänt dåligt, och att lösa Foonys proxykompatibilitet var ganska knepigt.
Det här är inte heller ett fall av "Foony använder exotiska API:er" (även om vi gör det). Det var en kombination av:
- Proxyservrar som gör aggressiv strängomskrivning på platser där de absolut inte borde göra det.
- Proxyservrar som behandlar huvudwebbplatsens domän annorlunda än "andra" domäner (CDN:er, asset-värdar osv.).
- Och den hårda verkligheten att vissa proxyservrar helt enkelt inte kan stödja moderna webbappar (HTTPS-korrekthet, WebSockets osv.).
Vi fungerar inte med alla proxyservrar, men vi fungerar nu åtminstone med croxyproxy och proxyorb, vilket var målet.
Nedan förklarar jag vad som gick sönder, varför det gick sönder, och vilka fixar som faktiskt gjorde skillnad.
Omgång 1: Giltiga, trasiga Three.js-shaders
Symtomet
När jag testade croxyproxy kunde jag inte spela 8 Ball Pool eller några av Foonys andra three.js spel. Jag fick hela tiden ett shaderkompileringsfel i Three.js med fel som:
- "Shader Error 1282 - VALIDATE_STATUS false"
Det meddelandet var nästan helt värdelöst. Det betyder vanligtvis "din shader är ogiltig, lycka till." Toppen. Om du någonsin undrar varför jag alltid använder unika felmeddelanden för varje enskilt fel på Foony, så är det därför. Det hjälper till att lokalisera problem istället för bara "koden är trasig, gå och fixa."
Men varför gick helt giltiga three.js-shaders sönder? Vad är grejen?
Den faktiska orsaken: proxyservrar som korrumperar layout(location = N)
Three.js skickar ut GLSL med layout-kvalificerare som:
layout(location = 0) in vec3 position;
Vissa proxyservrar försöker skriva om allt som ser ut som JavaScript-API:et location genom att göra naiv global strängersättning. Det är redan illa i JS, men de gjorde det också inuti shader-källkodssträngar. Jag antar att AST-parsning är för dyrt för dem.
Så shader-källkoden blev korrumperad till något som:
layout(__cpLocation = 0) in vec3 position;
Identifieraren måste vara location där. Allt annat är ogiltig GLSL, och kompilatorn avvisar det. (Layout Qualifiers in GLSL)
Detta är ett Three.js-problem endast i den bemärkelsen att Three.js genererar shaders dynamiskt, och vi skickar dem till WebGL vid körning. Den verkliga buggen är proxyserverns omskrivningsstrategi.
Varför jag inte "fixade proxyservern"
Ett naivt tillvägagångssätt skulle vara att leta efter croxyproxys location-ersättningssträng, __cpLocation, och ersätta den med location. Men olika proxyservrar använder olika ersättningsnamn. Vissa använder __cpLocation, andra använder andra konstiga identifierare. Så att hårdkoda en fix som "ersätt __cpLocation tillbaka till location" är skört.
Jag behövde:
- En generisk fix (ingen hårdkodning av proxyidentifierare).
- En fix som fungerar även om proxyservern skriver om ordet
locationi min JavaScript också.
Base64-tricket: dölj ordet location från proxyservern
Om proxyservern skriver om varje bokstavligt location den ser, är det enklaste draget att helt enkelt inte använda location. Lätt nog. Jag har sett knep för detta tidigare i Lua när jag reverse-engineerade RestedXP:s guidesskydd (om jag minns rätt obfuskerar de sin användning av BNGetInfo, t.ex. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Det här tricket fungerar förstås även i JavaScript. I client/index.html avkodar jag följande vid körning:
// Eftersom dessa proxyservrar försöker ersätta varje `location`, använder vi en base64-kodad sträng.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Det där atob() händer efter att proxyservern redan har gjort sin HTML/JS-omskrivning, så den kan inte "förkorrumpera" strängen. Jag delar upp strängen i två delar för att göra det ännu svårare att upptäcka, och jag använder 'atob' för att jag kan, men String.fromCharCode eller hex-escape window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] skulle också kunna fungera.
Det trasiga shader-mönstret är alltid strukturellt detsamma:
layout(<något> = <nummer>)
Så jag matchar det generiskt och ersätter <något> med rätt identifierare:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
WebGL-kroken: patcha shaderSource (WebGL1 + WebGL2)
Eftersom Three.js anropar gl.shaderSource(shader, source), patchar jag shaderSource själv:
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,
});
Och jag tillämpar samma patch på WebGL2RenderingContext om den finns.
När det väl var på plats försvann shaderkompileringsfelen. Vid det här laget fungerade croxyproxy men proxyorb misslyckades fortfarande. Varför?! Borde inte det fungera på samma sätt?
Omgång 2: Det andra problemet (domäner) och varför det blev mycket enklare när jag tog bort foony.io
Foony har historiskt sett använt två domäner, åtminstone den senaste månaden:
foony.comför huvudwebbplatsenfoony.ioför statiska resurser
Den ursprungliga anledningen var praktisk: att servera resurser från en cookielös domän undviker uppsvälld cookie-header-uppladdning vid varje statisk filförfrågan. Det är bra, men inte så nödvändigt som man kan tro med tanke på att HTTP/2 använder HPACK för att minska antalet bytes som skickas över linan för headers.
Det är en giltig optimering vid normal surfning.
Bakom proxyservrar blev det en stor källa till problem. Och Foonys användarbas älskar proxyservrar. suck
Proxyservrar behandlar "huvudwebbplatsen" annorlunda än "andra webbplatser"
Många proxyservrar är optimerade för "proxa den här ena sidan / domänen." De laddar huvud-HTML:en framgångsrikt, injicerar skript, registrerar sin egen ServiceWorker osv.
Men när appen börjar hämta resurser från ett annat origin (som foony.io) hamnar man i alla möjliga roliga trasigheter:
- ServiceWorker-avbrottsfel som:
- "ServiceWorker intercepted the request and encountered an unexpected error"
- "Loading failed for the module with source"
- Query-parametrar som krävs av proxyinfrastrukturen (och lätt att råka strippa).
- Resursförfrågningar som tappar proxyserverns interna routing-metadata.
- Konstiga proxyservrar som ersätter hela förfrågan med
generic-php-slug.php?someQueryParam=hugeEncodedString(ja, jag brydde mig inte om att stödja det).
Dessa proxyservrars interna mekanismer är beroende av query-parametrar / URL-kodning, och de är ganska sköra.
Ett av de avslöjande exemplen var en resurs-URL som:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Det där ?__pot=... är proxyserverns egna routing/state som talar om för proxyservern vilken domän förfrågan gäller. Om man strippar det kan proxyservrar inte lösa förfrågan korrekt, och man hamnar i ServiceWorker-felvägen.
"Resursbyte" till räddningen (och varför det blev komplicerat fort)
Vid ett tillfälle försökte jag en lösning: upptäck "vi är proxade", och byt sedan ut alla foony.io-resurs-URL:er till det aktuella originet så att proxyservern skulle se allt som same-origin.
Det låter rimligt, och det fungerade för croxyproxy, men det tillförde mycket komplexitet:
- Du måste ersätta
link- ochscript-taggar som redan finns i HTML:en. - Du behöver en
MutationObserverför att hantera dynamiskt injicerade taggar (modulepreload, stylesheet osv.). - Du måste bevara proxyserverns query-parametrar, annars förstör du deras routing. Och olika proxyservrar gör detta olika. För det gör de förstås.
- Och du måste ändå hålla logiken generisk (inga proxyspecifika globaler) så att koden inte blir en uppsvälld soptipp.
Det är också här "base64-tricket" kom upp igen: även i min egen JavaScript var jag tvungen att vara försiktig med den bokstavliga strängen location eftersom proxyservern kanske skriver om den.
Reverse-engineering av CroxyProxys injicerade skript
Vid det här laget blev jag nyfiken: vad gör proxyservern egentligen med min sida? Injicerar den sina egna annonser? Något värre?
CroxyProxys klientsideskript är kraftigt obfuskerat.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Vilket när det körs resulterar i:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Baserat på detta ser det ut som att croxyproxy använder Obfuscator.io för denna obfuskering. Vilket tack och lov är lätt nog att deobfuskera med webcrack.
Detta resulterar i mycket mer 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)) {
Najs. Nu kan vi se vad den gör. Och... det verkar mestadels okej. Jag tror att obfuskeringen mest är till för att förhindra upptäckt av proxyservern. Mestadels.
Vi har lite annons- / 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;
}
Det finns några andra ställen, men i grund och botten visar den bara annonser, inklusive pop-under-stilade annonser. Den använder också FuckAdBlock.
Den verkliga ersättningen av strängarna sker dock på serversidan. Och vem vet vad allt det gör.
Hur som helst, du borde absolut inte använda webbproxyservrar om du bryr dig om din kontosäkerhet. Om du måste, undvik att ange några av dina personuppgifter / kontoinformation / köpinformation.
"Resursbyte" i papperskorgen
Jag bestämde mig för att komplexiteten från resursbyte, kombinerad med komplexiteten i andra delar av koden för foony.io-stöd, inte var värd de små nätverksbesparingarna från vackra, cookielösa förfrågningar. Vi såg också en oförklarlig minskning av våra spelkonverteringar sedan vi införde foony.io, så jag misstänker att det fanns andra problem med foony.io som vi inte var medvetna om.
Så jag tog bort foony.io. Åtminstone för tillfället.
När jag väl raderade foony.io CDN-logiken och standardiserade allt på foony.com, blev proxystödet dramatiskt enklare:
- Same-origin-resursladdningar.
- Färre "specialfall" att förklara för en proxy-ServiceWorker.
- Mindre omskrivning.
- Mindre skör kod.
Kort sagt, att ta bort foony.io var en arkitektonisk förenkling som minskade ytan för konstigt proxybeteende.
Omgång 3: Vad som fungerar, vad som inte fungerar, och varför
Bekräftade fungerande proxyservrar
Vid det här laget fungerar Foony bakom:
- croxyproxy
- proxyorb
Vissa andra proxyservrar fungerar förmodligen också. Jag slår vad om att de flesta fortfarande inte gör det. Men åtminstone de viktiga som folk använder för att spela spel på verkar fungera.
Varför inte "alla proxyservrar"?
Vissa proxyservrar kan helt enkelt inte stödja en modern multiplayer-webbapp. Exempel:
- Proxyservrar som inte stödjer HTTPS korrekt.
- Proxyservrar som bryter eller blockerar WebSockets (Foony använder realtidsnätverk). Tekniskt sett skulle man kunna komma runt detta, men det skulle lägga till komplexitet.
- Proxyservrar som har för många begränsningar kring cross-origin-förfrågningar, headers, eller ServiceWorkers.
Viktiga lärdomar
Webbproxyservrar är mycket osäkra
De är middleware som:
- skriver om HTML
- skriver om JavaScript
- ibland injicerar en ServiceWorker
- och ofta är beroende av query-parametrar / URL-kodning för att routa förfrågningar
- kan fippla med dina sidor på vilket sätt som helst
Jag blev förvånad över hur djupt vissa proxyservrar går: de skriver om shader-källkodssträngar, kommentarer, och Gud vet vad mer.
Ibland är den bästa fixen arkitektonisk
WebGL-patchen fick spelen att rendera igen, men att ta bort multidomän-CDN-strategin gjorde att proxystödet förblev stabilt.
Det är en bra påminnelse: smarta optimeringar kan vara helt rimliga tills de krockar med fientlig middleware. Eller användarens webbläsartillägg. Eller Safari. Eller språkinställningar. Eller tillgänglighetsfunktioner. Eller solstormar. Eller vad som helst, egentligen.
Slutsats
Foony fungerar nu bakom de proxyservrar som spelar roll (croxyproxy och proxyorb), utan att förvandla kodbasen till en proxyspecifik röra:
- En generisk Three.js-shaderfix (inga proxyspecifika identifierare).
- En enklare domänstrategi (foony.com överallt).