

1/1/1970
Wie ich Foony hinter Proxys zum Laufen gebracht habe
Hallo zusammen! Dass Web-Proxys bei Websites Kompatibilitätsprobleme verursachen, weiß ich schon lange. Allerdings war Foonys Unterstützung in Proxys notorisch schlecht, und die Proxy-Kompatibilität von Foony zu lösen, war ziemlich knifflig.
Das ist auch kein „Foony nutzt exotische APIs"-Problem (auch wenn wir das tun). Es war eine Kombination aus:
- Proxys, die aggressives String-Rewriting an Stellen machen, an denen sie das absolut nicht tun sollten.
- Proxys, die die Haupt-Site-Domain anders behandeln als „andere" Domains (CDNs, Asset-Hosts usw.).
- Und die harte Realität, dass manche Proxys einfach keine modernen Web-Apps unterstützen können (HTTPS-Korrektheit, WebSockets usw.).
Wir funktionieren nicht mit jedem Proxy, aber inzwischen klappt es zumindest mit croxyproxy und proxyorb, was das Ziel war.
Im Folgenden erkläre ich, was kaputt war, warum es kaputt war, und welche Lösungen wirklich entscheidend waren.
Durchgang 1: Gültige, kaputte Three.js-Shader
Das Symptom
Als ich croxyproxy ausprobiert habe, konnte ich 8 Ball Pool oder irgendeines von Foonys anderen three.js Spielen nicht spielen. Ich bekam ständig einen Shader-Kompilierungsfehler in Three.js mit Meldungen wie:
- „Shader Error 1282 - VALIDATE_STATUS false"
Diese Meldung war fast völlig nutzlos. Sie bedeutet üblicherweise „dein Shader ist ungültig, viel Glück." Toll. Falls du dich je gefragt hast, warum ich auf Foony für jeden einzelnen Fehler immer einzigartige Fehlermeldungen verwende, hier hast du den Grund. Es hilft, Probleme zu lokalisieren, statt nur „Code ist kaputt, mach's heile."
Aber warum brachen perfekt gültige three.js-Shader? Was war hier los?
Die eigentliche Ursache: Proxys, die layout(location = N) korrumpieren
Three.js erzeugt GLSL mit Layout-Qualifizierern wie:
layout(location = 0) in vec3 position;
Manche Proxys versuchen, alles, was nach der JavaScript-location-API aussieht, durch naives globales String-Replacement umzuschreiben. Das ist schon in JS schlimm, aber sie haben das auch innerhalb von Shader-Quellstrings gemacht. Ich vermute, AST-Parsing ist ihnen zu teuer.
Also wurde der Shader-Quelltext zu etwas wie diesem korrumpiert:
layout(__cpLocation = 0) in vec3 position;
Der Bezeichner muss dort location sein. Alles andere ist ungültiges GLSL, und der Compiler lehnt es ab. (Layout Qualifiers in GLSL)
Das ist nur insofern ein Three.js-Problem, als Three.js Shader dynamisch generiert und wir sie zur Laufzeit an WebGL übergeben. Der eigentliche Bug liegt in der Rewriting-Strategie des Proxys.
Warum ich nicht „den Proxy gefixt" habe
Ein naiver Ansatz wäre, nach croxyproxys location-Ersatzstring __cpLocation zu suchen und ihn durch location zu ersetzen. Allerdings verwenden verschiedene Proxys verschiedene Ersatznamen. Manche nutzen __cpLocation, andere nutzen andere seltsame Bezeichner. Eine Lösung wie „ersetze __cpLocation zurück zu location" hart zu codieren, ist also fragil.
Ich brauchte:
- Eine generische Lösung (kein Hardcoding von Proxy-Bezeichnern).
- Eine Lösung, die auch funktioniert, wenn der Proxy das Wort
locationin meinem JavaScript ebenfalls umschreibt.
Der Base64-Trick: das Wort location vor dem Proxy verstecken
Wenn der Proxy jedes wörtliche location, das er sieht, umschreibt, ist der einfachste Schritt, einfach kein location zu verwenden. Easy. Solche Tricks habe ich schon mal in Lua gesehen, als ich RestedXPs Guide-Schutzsystem reverse-engineered habe (wenn ich mich richtig erinnere, verschleiern sie ihre Verwendung von BNGetInfo, z. B. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Dieser Trick funktioniert natürlich auch in JavaScript. In client/index.html dekodiere ich Folgendes zur Laufzeit:
// 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
Das atob() passiert nachdem der Proxy bereits sein HTML/JS-Rewriting abgeschlossen hat, also kann er den String nicht „vorab korrumpieren". Ich teile den String in zwei Teile auf, um es noch schwerer zu erkennen, und ich nutze 'atob', weil ich es kann, aber String.fromCharCode oder Hex-Escaping wie window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] könnten ebenfalls funktionieren.
Das kaputte Shader-Muster ist strukturell immer dasselbe:
layout(<something> = <number>)
Also matche ich das generisch und ersetze <something> durch den korrekten Bezeichner:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
Der WebGL-Hook: shaderSource patchen (WebGL1 + WebGL2)
Da Three.js gl.shaderSource(shader, source) aufruft, patche ich shaderSource selbst:
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,
});
Und ich wende denselben Patch auf WebGL2RenderingContext an, falls es existiert.
Sobald das in Position war, verschwanden die Shader-Kompilierungsfehler. Zu diesem Zeitpunkt funktionierte croxyproxy, aber proxyorb scheiterte immer noch. Wieso?! Sollte das nicht gleich funktionieren?
Durchgang 2: Das zweite Problem (Domains) und warum das Entfernen von foony.io alles einfacher gemacht hat
Foony hat historisch zwei Domains verwendet, zumindest im letzten Monat:
foony.comfür die Hauptseitefoony.iofür statische Assets
Der ursprüngliche Grund war praktisch: Assets von einer cookielosen Domain auszuliefern, vermeidet Cookie-Header-Upload-Aufblähung bei jedem statischen Datei-Request. Das ist klasse, aber nicht so nötig, wie man denken könnte, da HTTP/2 HPACK verwendet, um Bytes für Header über die Leitung zu reduzieren.
Es ist eine valide Optimierung beim normalen Surfen.
Hinter Proxys wurde es zu einer Hauptquelle von Defekten. Und Foonys Userbase liebt Proxys. Seufz.
Proxys behandeln „die Hauptseite" anders als „andere Seiten"
Viele Proxys sind für „proxy diese eine Seite / Domain" optimiert. Sie laden das Haupt-HTML erfolgreich, injizieren Skripte, registrieren ihren eigenen ServiceWorker usw.
Aber wenn die App anfängt, Assets von einem anderen Origin (wie foony.io) zu ziehen, gerät man in allerlei lustige Defekt-Spielereien:
- ServiceWorker-Interception-Fehler wie:
- „ServiceWorker intercepted the request and encountered an unexpected error"
- „Loading failed for the module with source"
- Query-Parameter, die von der Proxy-Infrastruktur erforderlich sind (und leicht versehentlich gestrippt werden).
- Asset-Requests, die die internen Routing-Metadaten des Proxys verlieren.
- Seltsame Proxys, die den gesamten Request durch
generic-php-slug.php?someQueryParam=hugeEncodedStringersetzen (ja, das zu unterstützen, hab ich mir gespart).
Diese internen Mechanismen der Proxys hängen von Query-Parametern / URL-Encoding ab, und sie sind ziemlich fragil.
Eines der verräterischen Beispiele war eine Asset-URL wie:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Das ?__pot=... ist der eigene Routing-/Zustandsparameter des Proxys, der dem Proxy mitteilt, für welche Domain der Request bestimmt ist. Wenn man ihn strippt, können Proxys den Request nicht korrekt auflösen, und man landet auf dem ServiceWorker-Fehlerpfad.
„Resource Swapping" zur Rettung (und warum es schnell kompliziert wurde)
An einem Punkt habe ich einen Workaround versucht: erkennen „wir werden geproxyt" und dann alle foony.io-Resource-URLs auf den aktuellen Origin umtauschen, damit der Proxy alles als same-origin sieht.
Klingt vernünftig, und für croxyproxy funktionierte es, aber es brachte viel Komplexität:
- Man muss
link- undscript-Tags ersetzen, die bereits im HTML existieren. - Man braucht einen
MutationObserver, um dynamisch injizierte Tags zu behandeln (modulepreload, stylesheet usw.). - Man muss die Query-Parameter des Proxys erhalten, sonst zerstört man dessen Routing. Und verschiedene Proxys machen das unterschiedlich. Weil natürlich.
- Und man muss die Logik trotzdem generisch halten (keine proxyspezifischen Globals), damit der Code nicht zu einem aufgeblähten Müllhaufen wird.
Hier kam auch der „Base64-Trick" wieder ins Spiel: selbst in meinem eigenen JavaScript musste ich vorsichtig mit dem wörtlichen String location sein, weil der Proxy ihn umschreiben könnte.
Reverse Engineering von CroxyProxys injiziertem Skript
An diesem Punkt wurde ich neugierig: was macht der Proxy eigentlich mit meiner Seite? Injiziert er seine eigenen Werbeanzeigen? Etwas Schlimmeres?
CroxyProxys clientseitiges Skript ist stark obfuskiert.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Wenn man das ausführt, ergibt sich:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Basierend darauf sieht es so aus, als ob croxyproxy Obfuscator.io für diese Verschleierung verwendet. Das lässt sich glücklicherweise leicht mit webcrack deobfuskieren.
Das ergibt deutlich besser lesbares 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)) {
Schön. Jetzt sieht man, was er macht. Und... es scheint größtenteils okay. Ich denke, die Verschleierung dient hauptsächlich dazu, die Erkennung des Proxys zu verhindern. Größtenteils.
Wir haben einige Werbe- / 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;
}
Es gibt noch einige andere Stellen, aber im Grunde zeigt es nur Werbung, einschließlich Pop-under-Anzeigen. Es nutzt auch FuckAdBlock.
Der eigentliche Ersatz der Strings passiert allerdings serverseitig. Und wer weiß, was das alles macht.
Wie auch immer, du solltest auf jeden Fall keine Web-Proxys verwenden, wenn dir deine Account-Sicherheit wichtig ist. Wenn du musst, vermeide es, irgendwelche persönlichen Daten / Account- / Kaufinformationen einzugeben.
„Resource Swapping" in den Mülleimer
Ich entschied, dass die Komplexität durch Resource Swapping, kombiniert mit der Komplexität in anderen Teilen des Codes für foony.io-Unterstützung, die kleinen Netzwerk-Einsparungen durch schöne, cookielose Requests nicht wert war. Wir haben außerdem einen unerklärlichen Rückgang unserer Gameplay-Conversions gesehen, seit wir foony.io eingeführt haben, also vermute ich, dass es noch andere Probleme mit foony.io gab, von denen wir nichts wussten.
Also habe ich foony.io entfernt. Zumindest erstmal.
Sobald ich die foony.io-CDN-Logik gelöscht und alles auf foony.com standardisiert habe, wurde die Proxy-Unterstützung dramatisch einfacher:
- Same-Origin-Asset-Loads.
- Weniger „Sonderfälle", die einem Proxy-ServiceWorker erklärt werden müssen.
- Weniger Rewriting.
- Weniger fragiler Code.
Kurz gesagt: das Entfernen von foony.io war eine architektonische Vereinfachung, die die Angriffsfläche für seltsames Proxy-Verhalten reduziert hat.
Durchgang 3: Was funktioniert, was nicht, und warum
Bestätigt funktionierende Proxys
Stand jetzt funktioniert Foony hinter:
- croxyproxy
- proxyorb
Einige andere Proxys funktionieren wahrscheinlich auch. Ich wette, die meisten immer noch nicht. Aber zumindest die wichtigen, mit denen Leute Spiele spielen, scheinen zu funktionieren.
Warum nicht „alle Proxys"?
Manche Proxys können einfach keine moderne Multiplayer-Web-App unterstützen. Beispiele:
- Proxys, die HTTPS nicht ordentlich unterstützen.
- Proxys, die WebSockets brechen oder blockieren (Foony nutzt Echtzeit-Networking). Technisch könnte man das umgehen, aber das würde Komplexität hinzufügen.
- Proxys, die zu viele Einschränkungen rund um Cross-Origin-Requests, Header oder ServiceWorker haben.
Wichtigste Erkenntnisse
Web-Proxys sind sehr unsicher
Sie sind Middleware, die:
- HTML umschreibt
- JavaScript umschreibt
- manchmal einen ServiceWorker injiziert
- und oft von Query-Parametern / URL-Encoding abhängt, um Requests zu routen
- auf vielfältigste Weise an deinen Seiten herumbasteln kann
Ich war überrascht, wie tief manche Proxys eingreifen: sie schreiben Shader-Quellstrings, Kommentare und Gott weiß was sonst noch um.
Manchmal ist die beste Lösung architektonisch
Der WebGL-Patch hat die Spiele wieder zum Rendern gebracht, aber das Entfernen der Multi-Domain-CDN-Strategie hat dafür gesorgt, dass die Proxy-Unterstützung stabil bleibt.
Eine gute Erinnerung: clevere Optimierungen können vollkommen vernünftig sein, bis sie mit feindseliger Middleware kollidieren. Oder mit Browser-Erweiterungen der User. Oder mit Safari. Oder mit Spracheinstellungen. Oder mit Barrierefreiheits-Features. Oder mit Sonneneruptionen. Oder eigentlich mit allem.
Fazit
Foony funktioniert jetzt hinter den Proxys, die zählen (croxyproxy und proxyorb), ohne die Codebase in ein proxyspezifisches Chaos zu verwandeln:
- Eine generische Three.js-Shader-Lösung (keine proxyspezifischen Bezeichner).
- Eine einfachere Domain-Strategie (überall foony.com).