

1/1/1970
Jak jsem rozchodil Foony za proxy
Zdravíčko! Už dlouho vím, že webové proxy způsobují weby problémy s kompatibilitou. Podpora Foony v proxy ale byla pověstně mizerná a vyřešit kompatibilitu Foony s proxy bylo docela ošemetné.
Nejde ani o problém typu „Foony používá exotická API" (i když používáme). Šlo o kombinaci:
- Proxy agresivně přepisují řetězce na místech, kde by absolutně neměly.
- Proxy zacházejí s hlavní doménou webu jinak než s „ostatními" doménami (CDN, hostingy assetů atd.).
- A drsná realita, že některé proxy prostě nedokáží podporovat moderní webové aplikace (správné HTTPS, WebSockety atd.).
Nepracujeme s každou proxy, ale teď fungujeme alespoň s croxyproxy a proxyorb, což byl cíl.
Níže vysvětlím, co se rozbilo, proč se to rozbilo a které opravy skutečně něco znamenaly.
Pokus 1: Validní, ale rozbité Three.js shadery
Příznak
Když jsem zkusil croxyproxy, nemohl jsem si zahrát 8 Ball Pool ani žádnou další three.js hru na Foony. Pořád jsem dostával chybu kompilace shaderu v Three.js s hláškami jako:
- „Shader Error 1282 - VALIDATE_STATUS false"
Ta zpráva byla skoro k ničemu. Obvykle znamená „tvůj shader je neplatný, hodně štěstí." Super. Pokud se někdy divíte, proč na Foony vždy používám unikátní chybové hlášky pro každou jednotlivou chybu, tak právě proto. Pomáhá to lokalizovat problémy místo prostého „kód je rozbitý, jdi to spravit."
Ale proč se rozbíjely naprosto validní three.js shadery? Co se to děje?
Skutečná příčina: proxy poškozují layout(location = N)
Three.js generuje GLSL s layout kvalifikátory jako:
layout(location = 0) in vec3 position;
Některé proxy se pokoušejí přepsat cokoliv, co vypadá jako JavaScriptové location API, naivním globálním nahrazením řetězců. To je samo o sobě špatné v JS, ale dělali to i uvnitř zdrojových řetězců shaderů. Asi je AST parsování pro ně příliš drahé.
Takže zdrojový kód shaderu byl poškozen na něco jako:
layout(__cpLocation = 0) in vec3 position;
Identifikátor tam musí být location. Cokoliv jiného je neplatný GLSL a kompilátor to odmítne. (Layout kvalifikátory v GLSL)
Tohle je problém Three.js jen v tom smyslu, že Three.js generuje shadery dynamicky a my je předáváme WebGL za běhu. Skutečný bug je strategie přepisování proxy.
Proč jsem „neopravil proxy"
Naivní přístup by byl hledat náhradní řetězec croxyproxy pro location, tedy __cpLocation, a nahradit ho zpět za location. Jenže různé proxy používají různé náhradní názvy. Některé používají __cpLocation, jiné jiné podivné identifikátory. Takže napevno zakódovaná oprava typu „nahraď __cpLocation zpět za location" je křehká.
Potřeboval jsem:
- Generickou opravu (žádné napevno zakódované identifikátory proxy).
- Opravu, která funguje, i když proxy přepisuje slovo
locationi v mém JavaScriptu.
Trik s base64: skrýt slovo location před proxy
Pokud proxy přepisuje každý literál location, který vidí, nejjednodušší řešení je prostě location nepoužívat. Snadné. Podobné triky jsem už viděl v Lue, když jsem reverzně analyzoval ochranu průvodců RestedXP (jestli si vzpomínám správně, obfuskovali použití BNGetInfo, např. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Tento trik funguje samozřejmě i v JavaScriptu. V client/index.html dekóduji za běhu následující:
// Protože se tyto proxy snaží nahradit každý `location`, používáme řetězec zakódovaný v base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Ten atob() se spustí až poté, co proxy provedla své přepisování HTML/JS, takže nemůže řetězec „předem poškodit." Řetězec jsem rozdělil na dvě části, aby byl ještě hůře detekovatelný, a 'atob' používám, protože můžu, ale String.fromCharCode nebo hex-escapování window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] by také mohlo fungovat.
Vzor poškozeného shaderu má vždy strukturálně stejnou podobu:
layout(<něco> = <číslo>)
Takže to obecně namatchuju a <něco> nahradím správným identifikátorem:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
WebGL hook: patchnout shaderSource (WebGL1 + WebGL2)
Protože Three.js volá gl.shaderSource(shader, source), patchuju samotný 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 aplikuju na WebGL2RenderingContext, pokud existuje.
Jakmile to bylo na svém místě, chyby kompilace shaderů zmizely. V tu chvíli croxyproxy fungoval, ale proxyorb pořád selhával. Proč?! Nemělo by to fungovat stejně?
Pokus 2: Druhý problém (domény) a proč odstranění foony.io všechno usnadnilo
Foony historicky používalo dvě domény, alespoň poslední měsíc:
foony.compro hlavní webfoony.iopro statické assety
Původní důvod byl praktický: servírování assetů z domény bez cookies se vyhne nafouknutí cookie hlaviček při každém požadavku na statický soubor. To je skvělé, ale ne tak nutné, jak by se zdálo, vzhledem k tomu, že HTTP/2 používá HPACK ke snížení počtu bajtů odesílaných po drátě pro hlavičky.
Pro normální prohlížení je to validní optimalizace.
Za proxy se to ale stalo hlavním zdrojem rozbití. A uživatelská základna Foony proxy miluje. povzdech
Proxy zacházejí s „hlavním webem" jinak než s „ostatními weby"
Mnoho proxy je optimalizováno pro „proxy téhle jedné stránky / domény." Úspěšně načtou hlavní HTML, injektují skripty, registrují vlastní ServiceWorker atd.
Ale když aplikace začne tahat assety z jiného originu (jako foony.io), dostanete se do nejrůznějších zábavných rozbití:
- Selhání zachycení ServiceWorkerem jako:
- „ServiceWorker intercepted the request and encountered an unexpected error"
- „Loading failed for the module with source"
- Query parametry vyžadované infrastrukturou proxy (a snadno se omylem strhnou).
- Požadavky na assety ztrácejí interní routovací metadata proxy.
- Podivné proxy, které nahradí celý požadavek za
generic-php-slug.php?someQueryParam=hugeEncodedString(jo, tohle podporovat jsem si nedal práci).
Tyto interní mechanismy proxy závisejí na query parametrech / URL kódování a jsou docela křehké.
Jedním z příznačných příkladů byla URL assetu jako:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Ten ?__pot=... je vlastní routování/stav proxy, který říká, pro kterou doménu je požadavek určen. Když to strhnete, proxy nedokáže požadavek správně vyhodnotit a skončíte v cestě s chybou ServiceWorkeru.
„Resource swapping" zachraňuje (a proč se to rychle zkomplikovalo)
V jednu chvíli jsem zkusil obejít to takhle: detekovat „jsme za proxy" a pak zaměnit všechny URL zdrojů foony.io za aktuální origin, aby proxy viděla všechno jako same-origin.
Zní to rozumně a fungovalo to pro croxyproxy, ale přidalo to spoustu složitosti:
- Musíte nahradit
linkascripttagy, které už v HTML existují. - Potřebujete
MutationObserver, abyste zvládli dynamicky injektované tagy (modulepreload, stylesheet atd.). - Musíte zachovat query parametry proxy, jinak rozbijete jejich routování. A různé proxy to dělají různě. No samozřejmě.
- A pořád musíte držet logiku obecnou (žádné globály specifické pro proxy), aby se kód nestal nafouklou katastrofou.
Tady se zase objevil „base64 trik": dokonce i ve svém vlastním JavaScriptu jsem si musel dávat pozor na literální řetězec location, protože by ho proxy mohla přepsat.
Reverzní inženýrství skriptu injektovaného CroxyProxy
V tu chvíli jsem dostal chuť zjistit: co vlastně proxy s mojí stránkou dělá? Injektuje vlastní reklamy? Něco horšího?
Klientský skript CroxyProxy je silně obfuskovaný.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Po spuštění to vede k:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Podle toho to vypadá, že croxyproxy používá k této obfuskaci Obfuscator.io. Což je naštěstí dost snadné deobfuskovat pomocí webcrack.
Výsledek je 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)) {
Pěkné. Teď vidíme, co to dělá. A... vypadá to víceméně v pořádku. Myslím, že obfuskace má hlavně pomoct zabránit detekci proxy. Hlavně.
Máme tu nějaké injektová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 podstatě jen zobrazuje reklamy, včetně pop-under stylu reklam. Také používá FuckAdBlock.
Skutečné nahrazování řetězců se ale děje na straně serveru. A kdo ví, co všechno tam dělá.
Tak jako tak, rozhodně byste neměli používat webové proxy, pokud vám záleží na bezpečnosti vašeho účtu. Pokud musíte, vyhněte se zadávání jakýchkoliv osobních údajů / účtu / platebních informací.
„Resource swapping" do koše
Rozhodl jsem se, že složitost resource swappingu, spolu se složitostí v jiných částech kódu kvůli podpoře foony.io, nestála za malou síťovou úsporu krásných požadavků bez cookies. Také jsme od přijetí foony.io viděli nevysvětlitelný pokles konverzí her, takže mám podezření, že s foony.io byly i další problémy, o kterých jsme nevěděli.
Tak jsem foony.io odstranil. Aspoň prozatím.
Jakmile jsem smazal logiku CDN pro foony.io a všechno standardizoval na foony.com, podpora proxy se dramaticky zjednodušila:
- Same-origin načítání assetů.
- Méně „speciálních případů", které je třeba vysvětlovat ServiceWorkeru proxy.
- Méně přepisování.
- Méně křehký kód.
Stručně řečeno, odstranění foony.io bylo architektonické zjednodušení, které snížilo plochu pro podivné chování proxy.
Pokus 3: Co funguje, co ne a proč
Potvrzeně fungující proxy
V tuto chvíli Foony funguje za:
- croxyproxy
- proxyorb
Některé další proxy pravděpodobně fungují. Vsadím se, že většina pořád ne. Ale aspoň ty důležité, které lidé používají k hraní her, vypadají, že fungují.
Proč ne „všechny proxy"?
Některé proxy prostě nedokáží podporovat moderní multiplayerovou webovou aplikaci. Příklady:
- Proxy, které správně nepodporují HTTPS.
- Proxy, které rozbíjejí nebo blokují WebSockety (Foony používá real-time síťování). Technicky by se to dalo obejít, ale přidalo by to složitost.
- Proxy, které mají příliš mnoho omezení kolem cross-origin požadavků, hlaviček nebo ServiceWorkerů.
Klíčové poznatky
Webové proxy jsou velmi nebezpečné
Jsou to mezivrstvy, které:
- přepisují HTML
- přepisují JavaScript
- někdy injektují ServiceWorker
- a často závisejí na query parametrech / URL kódování pro směrování požadavků
- mohou si s vašimi stránkami pohrávat nesčetnými způsoby
Překvapilo mě, jak hluboko některé proxy zacházejí: přepisují zdrojové řetězce shaderů, komentáře a bůhvíco ještě.
Někdy je nejlepší oprava architektonická
WebGL patch zase rozjel hry, ale odstranění strategie multi-doménového CDN udrželo podporu proxy stabilní.
Je to dobrá připomínka: chytré optimalizace mohou být naprosto rozumné, dokud se nesrazí s nepřátelskou mezivrstvou. Nebo s rozšířeními prohlížeče uživatele. Nebo se Safari. Nebo s nastavením jazyka. Nebo s funkcemi přístupnosti. Nebo se slunečními erupcemi. Nebo s čímkoliv, vážně.
Závěr
Foony teď funguje za proxy, na kterých záleží (croxyproxy a proxyorb), aniž by se kódová báze proměnila v nepořádek specifický pro proxy:
- Generická oprava shaderů Three.js (žádné identifikátory specifické pro proxy).
- Jednodušší doménová strategie (foony.com všude).