

1/1/1970
Jak jsem Foony rozchodil přes proxy
Čau! Už dlouho vím, že webové proxy často dělají stránkám problémy s kompatibilitou. Podpora Foony přes proxy ale byla pověstně mizerná a vyřešit kompatibilitu Foony s proxynami bylo docela oříšek.
Nešlo přitom o problém typu „Foony používá nějaké exotické API“ (i když používáme). Byla to kombinace:
- Proxy agresivně přepisují řetězce i na místech, kde by na ně vůbec sahat neměly.
- Proxy zachází s hlavní doménou webu jinak než s „ostatními“ doménami (CDN, servery pro statická data atd.).
- A krutá realita, že některé proxy prostě vůbec nezvládají moderní webové aplikace (správné HTTPS, WebSockets atd.).
Neumíme fungovat za úplně každou proxy, ale teď běžíme aspoň za croxyproxy a proxyorb, což byl cíl.
Níž popíšu, co se rozbíjelo, proč se to rozbíjelo a jaké opravy nakonec opravdu hrály roli.
První kolo: Platné, ale rozbité shadery v Three.js
Projev problému
Když jsem zkusil croxyproxy, nešlo mi spustit 8 Ball Pool ani žádnou z Foony ostatních three.js her. Pořád jsem narážel na chybu při kompilaci shaderu v Three.js, něco jako:
- “Shader Error 1282 - VALIDATE_STATUS false”
To hlášení bylo skoro úplně k ničemu. V překladu obvykle znamená „tvůj shader je neplatný, hodně štěstí“. Paráda. Kdybys někdy přemýšlel, proč má na Foony každá chyba svoje vlastní unikátní chybové hlášení, tak právě proto. Pomáhá to trefit přímo na problém místo „něco je rozbité, tak to oprav“.
Ale proč se rozbíjely úplně validní shadery pro three.js? Co se to dělo?
Skutečná příčina: proxy kazí layout(location = N)
Three.js generuje GLSL se specifikátory layoutu jako:
layout(location = 0) in vec3 position;
Některé proxy se snaží přepsat všechno, co vypadá jako JavaScriptové API location, pomocí naivního globálního nahrazování řetězců. Už v samotném JS je to špatný nápad, ale ony to navíc dělaly i uvnitř zdrojáku shaderů. Zřejmě je pro ně parsování AST moc drahé.
Takže se zdroják shaderu zmrzačil na něco jako:
layout(__cpLocation = 0) in vec3 position;
Na tom místě musí být identifikátor location. Cokoliv jiného je neplatné GLSL a kompilátor to odmítne. (Layout Qualifiers in GLSL)
Z pohledu Three.js je problém jen v tom, že Three.js shadery generuje dynamicky a my je za běhu předáváme WebGL. Skutečný bug je v tom, jak proxy přepisuje text.
Proč jsem nechtěl „opravovat proxy“
Nabízelo se jednoduché řešení: najít v textu náhradu location, kterou používá croxyproxy, tedy __cpLocation, a přepsat ji zpátky na location. Jenže různé proxy používají různé názvy náhrad. Některé __cpLocation, jiné úplně jiné podivné identifikátory. Takže natvrdo zadrátované „nahraď __cpLocation zpět za location“ by bylo hodně křehké.
Potřeboval jsem:
- univerzální řešení (bez napevno zadrátovaných identifikátorů konkrétních proxy),
- řešení, které bude fungovat i v případě, že proxy přepisuje samotné slovo
locationi v mém JavaScriptu.
Fígl s base64: schovat slovo location před proxy
Když proxy přepisuje každý doslovný výskyt location, nejjednodušší je prostě location nepoužívat. Celkem snadné. Podobné triky jsem už viděl v Lua, když jsem reverzně analyzoval systém ochrany návodů v RestedXP (jestli si dobře pamatuju, tak si tam obfuskují volání BNGetInfo, třeba jako _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Tenhle trik samozřejmě funguje i v JavaScriptu. V client/index.html si za běhu dekóduju tohle:
// Protože tyhle proxy zkouší nahradit každé `location`, použijeme řetězec zakódovaný v base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
atob() se zavolá až po tom, co proxy udělá svoje přepisování HTML/JS, takže ten řetězec nemá šanci „předem zmršit“. Řetězec jsem navíc rozdělil na dvě části, aby se ještě hůř detekoval, a používám 'atob' prostě proto, že můžu, ale klidně by šlo použít i String.fromCharCode nebo hex escape window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'].
Rozbitý shader má vždycky stejný strukturální vzor:
layout(<něco> = <číslo>)
Takže ten vzor obecně najdu a <něco> nahradím správným identifikátorem:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
WebGL háček: patch shaderSource (WebGL1 + WebGL2)
Protože Three.js volá gl.shaderSource(shader, source), napatchoval jsem přímo 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,
});
A stejný patch pak aplikuju i na WebGL2RenderingContext, pokud existuje.
Jakmile tohle bylo nasazené, chyby při kompilaci shaderů zmizely. V tu chvíli už croxyproxy fungovalo, ale proxyorb pořád padal. Proč?! Nemělo by se to chovat stejně?
Druhé kolo: Problém s doménami a proč odstranění foony.io všechno zjednodušilo
Foony historicky používal dvě domény, minimálně poslední měsíc:
foony.compro hlavní webfoony.iopro statická data
Původní důvod byl praktický: když se statická data servírují z domény bez cookies, necpou se cookie hlavičky do každého requestu na soubor. To je super, ale není to tak nutné, jak by se mohlo zdát, zvlášť když HTTP/2 používá HPACK na zmenšení objemu hlaviček po drátu.
V běžném prohlížení je to úplně legitimní optimalizace.
Za proxy se z toho ale stal velký zdroj problémů. A uživatelé Foony proxy milují. ach jo
Proxy zachází s „hlavním webem“ jinak než s „ostatními“
Hodně proxy je optimalizovaných na režim „proxyuj mi tuhle jednu stránku / doménu“. Hlavní HTML načtou v pohodě, vstříknou svoje skripty, zaregistrují si vlastní ServiceWorker atd.
Jenže když aplikace začne tahat data z jiného originu (třeba z foony.io), rozjede se všelijaký zábavný bordel:
- chyby při zachytávání requestů v ServiceWorkeru, třeba:
- „ServiceWorker intercepted the request and encountered an unexpected error“
- „Loading failed for the module with source“
- query parametry jsou pro infrastrukturu proxy povinné (a je hodně snadné je omylem zahodit),
- requesty na statická data ztrácejí vnitřní směrovací metadata proxy,
- podivné proxy, které celý request přepíšou na
generic-php-slug.php?someQueryParam=hugeEncodedString(jo, do toho jsem se fakt nepouštěl).
Vnitřnosti těchhle proxy často stojí na query parametrech a nějakém jejich speciálním kódování v URL a celé je to dost křehké.
Jedním z typických příkladů byla URL na statický soubor ve tvaru:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Ten ?__pot=... je vlastní směrovací / stavová informace proxy, která jí říká, pro jakou doménu je daný request. Když ji odstraníš, proxy už request nesměruje správně a skončíš v chybové větvi ServiceWorkeru.
„Přehazování zdrojů“ na záchranu (a proč se to rychle zkomplikovalo)
V jednu chvíli jsem zkusil fígl: detekovat, že „jsme za proxy“, a pak všechny URL na zdroje z foony.io přepsat na aktuální origin, aby proxy všechno viděla jako same-origin.
Zní to docela rozumně a pro croxyproxy to fungovalo, ale přineslo to hromadu složitosti:
- musíš přepsat
linkascripttagy, které už v HTML existují, - potřebuješ
MutationObserver, abys zachytil dynamicky vkládané tagy (modulepreload, stylesheet atd.), - musíš zachovat query parametry, které si do URL přidává proxy, jinak rozbiješ jejich směrování. A různé proxy to dělají různě. Samozřejmě.
- a přitom celé to musí zůstat co nejvíc obecné (žádné globální proměnné typu „tohle je croxyproxy“), aby se z kódu nestala přerostlá skládka.
Tady se znovu hodil i „fígl s base64“: i ve vlastním JavaScriptu jsem musel dávat pozor na doslovný řetězec location, protože ho proxy mohla přepsat.
Reverzní inženýrství skriptu, který vpašuje CroxyProxy
V tu chvíli mě začalo zajímat: co mi vlastně ta proxy na stránce dělá? Vpichuje tam svoje reklamy? Nebo něco horšího?
Client-side skript croxyproxy je hodně obfuskovaný.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Když to pustíš, dostaneš něco jako:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Podle toho to vypadá, že croxyproxy používá na obfuskaci Obfuscator.io, což jde naštěstí docela snadno deobfuskovat pomocí webcrack.
Z toho pak vypadne mnohem čitelnější 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)) {
Hezké. Teď už je vidět, co to dělá. A... vypadá to v zásadě v pohodě. Myslím, že obfuskace je hlavně proto, aby se proxy hůř detekovala. Většinou.
Je tam nějaké vkládání reklam / 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;
}
Jsou tam i další místa, ale v zásadě jde hlavně o zobrazování reklam, včetně pop-under stylu. A používá to i FuckAdBlock.
Samotné přepisování řetězců ale ve skutečnosti probíhá na serveru. A kdo ví, co všechno tam běží.
Ať tak nebo tak, jestli ti aspoň trochu záleží na bezpečnosti účtů, webové proxy bys rozhodně používat neměl. A když už musíš, rozhodně přes ně nezadávej žádné osobní údaje / přihlašovací údaje / platební informace.
„Přehazování zdrojů“ letí do koše
Nakonec jsem si řekl, že složitost kolem přehazování zdrojů, navíc zkombinovaná se složitostí ostatních částí kódu kvůli podpoře foony.io, nestojí za těch pár ušetřených bajtů hezky čistých requestů bez cookies. Navíc jsme po nasazení foony.io viděli nevysvětlený propad konverzí do hry, takže tuším, že s foony.io byly ještě další problémy, o kterých jsme ani nevěděli.
Takže jsem foony.io prostě zrušil. Aspoň zatím.
Jakmile jsem vyhodil logiku pro CDN na foony.io a všechno sjednotil na foony.com, podpora proxy se znatelně zjednodušila:
- načítání zdrojů ze stejného originu,
- méně „speciálních případů“, které musí pochopit ServiceWorker proxy,
- méně přepisování,
- méně křehký kód.
Stručně: odstranění foony.io byla architektonická očista, která zmenšila plochu, kde se můžou projevit podivné chování proxy.
Třetí kolo: Co funguje, co ne a proč
Proxy, které ověřeně fungují
V tuhle chvíli Foony běží za:
- croxyproxy
- proxyorb
Některé další proxy nejspíš fungují taky. Vsadím se, že většina pořád ne. Ale aspoň ty důležité, přes které lidi typicky hrají hry, se zdají být v pohodě.
Proč ne „všechny proxy“?
Některé proxy prostě moderní multiplayerovou webovou aplikaci neutáhnou. Třeba:
- proxy, které neumí pořádně HTTPS,
- proxy, které rozbíjejí nebo blokují WebSockets (Foony používá real-time síťovou komunikaci). Technicky by se to dalo obejít, ale přidalo by to spoustu složitosti,
- proxy, které mají příliš přísná omezení kolem cross-origin requestů, hlaviček nebo ServiceWorkerů.
Hlavní poznatky
Webové proxy jsou hodně nebezpečné
Jsou to prostředníci, kteří:
- přepisují HTML,
- přepisují JavaScript,
- občas ti vrazí vlastní ServiceWorker,
- často se spoléhají na query parametry / kódování URL pro směrování requestů,
- můžou se hrabat v tvých stránkách skoro jak je napadne.
Překvapilo mě, jak hluboko některé proxy sahají: přepisují řetězce se zdrojákem shaderů, komentáře a Bůh ví co ještě.
Někdy je nejlepší opravou změna architektury
Patch pro WebGL sice způsobil, že se hry zase začaly vykreslovat, ale teprve odstranění multi‑doménové CDN strategie způsobilo, že podpora proxy zůstala stabilní.
Dobrá připomínka: chytré optimalizace můžou být úplně v pohodě, dokud nenarazí na nepřátelské middleware. Nebo na rozšíření v prohlížeči uživatele. Nebo na Safari. Nebo na jazykové nastavení. Nebo na funkce přístupnosti. Nebo na sluneční erupce. Nebo vlastně na cokoliv.
Závěr
Foony teď funguje za proxy, na kterých záleží (croxyproxy a proxyorb), aniž by se z kódu stal guláš plný speciálních případů pro každou proxy zvlášť:
- obecná oprava shaderů pro Three.js (žádné identifikátory konkrétních proxy),
- jednodušší strategie s doménami (všude foony.com).