background blurbackground mobile blur

1/1/1970

Paano Ko Pinagana ang Foony sa Likod ng Mga Proxy

Kamusta! Matagal ko nang alam na ang mga web proxy ay puwedeng magdulot ng mga problema sa compatibility ng mga website. Pero sobrang pangit talaga gumana ang Foony kapag dumadaan sa mga proxy, at medyo pahirapan ayusin ang proxy compatibility ng Foony.

Hindi rin ito dahil lang sa “Foony uses exotic APIs” na issue (kahit na meron nga). Kombinasyon siya ng:

  • Mga proxy na sobrang agresibo sa pagre-rewrite ng mga string sa mga lugar na hindi nila dapat galawin.
  • Mga proxy na iba ang trato sa pangunahing domain ng site kumpara sa mga “ibang” domain (CDN, asset host, atbp.).
  • At ang malupit na realidad na may mga proxy talagang hindi kakayanin ang modern web apps (tamang HTTPS, WebSockets, atbp.).

Hindi pa rin kami gumagana sa lahat ng proxy, pero gumagana na kami ngayon sa hindi bababa sa croxyproxy at proxyorb, na siya talagang target namin.

Sa ibaba, ikukuwento ko kung ano ang nasira, bakit nasira, at alin lang talaga sa mga fix ang may pakinabang.


Pass 1: Valid pero sirang Three.js shaders

Ang sintomas

Nang sinubukan ko ang croxyproxy, hindi ako makapaglaro ng 8 Ball Pool o ng iba pang mga three.js games ng Foony. Palagi akong nakakatanggap ng shader compilation failure sa Three.js na may mga error tulad ng:

  • “Shader Error 1282 - VALIDATE_STATUS false”

Halos walang silbi ang mensaheng ‘yan. Karaniwan ibig sabihin lang niyan ay “invalid ang shader mo, bahala ka na sa buhay mo.” Astig. Kung nagtataka ka kung bakit palagi akong may unique error message para sa bawat error sa Foony, ito ang dahilan. Mas madaling hanapin kung saan ang problema kaysa “sira ang code, ayusin mo.”

Pero bakit biglang nasisira ang mga perfectly valid na three.js shaders? Ano bang nangyayari?

Totoong dahilan: kino-corrupt ng proxy ang layout(location = N)

Naglalabas ang Three.js ng GLSL na may mga layout qualifier na ganito:

layout(location = 0) in vec3 position;

May mga proxy na sumusubok mag-rewrite ng kahit ano na parang JavaScript location API sa pamamagitan ng sobrang simpleng global string replacement. Masama na iyon sa JS pa lang, pero ginagawa pa nila ito sa loob ng shader source strings. Malamang para sa kanila, masyadong mahal ang gumawa ng AST parsing.

Kaya nagiging ganito ang shader source:

layout(__cpLocation = 0) in vec3 position;

Dapat talaga location ang identifier diyan. Kahit ano pang iba, invalid na GLSL iyon, at nirereject ng compiler. (Layout Qualifiers in GLSL)

Problema ito sa Three.js sa diwa na Three.js ang nagge-generate ng shaders nang dynamic, tapos ipapasa namin sa WebGL habang tumatakbo ang app. Pero ang totoong bug ay ang maling strategy ng proxy sa pagre-rewrite.

Bakit hindi ko “inayos ang proxy”

Isang simpleng approach ay hanapin ang replacement string ng croxyproxy para sa location, gaya ng __cpLocation, tapos palitan ulit ng location. Pero iba-iba ang replacement name ng bawat proxy. May gumagamit ng __cpLocation, may iba pang mas kakaibang identifier. Kaya ang hardcoded na fix na “palitan ang __cpLocation pabalik sa location” ay sobrang marupok.

Kailangan ko ng:

  • Generic na fix (walang hardcoded na proxy identifier).
  • Fix na gumagana kahit ang mismong salitang location sa JavaScript ko ay nirerewrite rin ng proxy.

Ang base64 trick: itago ang salitang location sa proxy

Kung nirerewrite ng proxy ang bawat literal na location na makita niya, pinakamadaling galaw ay huwag gumamit ng literal na location. Madali lang. Nakakita na ako ng ganitong trick dati sa Lua noong ni-reverse engineer ko ang guide protection system ng RestedXP (kung tama ang tanda ko, ino-obfuscate nila ang paggamit nila ng BNGetInfo, hal. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Gumagana rin ang trick na ‘to sa JavaScript, syempre. Sa client/index.html, dine-decode ko ito habang runtime:

// Dahil sinusubukan ng mga proxy na palitan ang bawat `location`, gumagamit tayo ng base64 encoded string.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

Nangyayari ang atob() pagkatapos gawin ng proxy ang HTML/JS rewriting niya, kaya hindi niya kayang “corrupt-in” nang maaga ang string. Hinihiwa-hiwalay ko pa ang string sa dalawa para mas mahirap pang hulaan, at gumagamit ako ng 'atob' dahil trip ko lang, pero puwede ring gumamit ng String.fromCharCode o hex-escaping tulad ng window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'].

Pare-pareho ang pattern ng sirang shader na nakikita ko:

layout(<something> = <number>)

Kaya minamatch ko iyon sa generic na paraan at pinapalitan ang <something> ng tamang identifier:

source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');

WebGL hook: patch sa shaderSource (WebGL1 + WebGL2)

Dahil tinatawag ng Three.js ang gl.shaderSource(shader, source), pinapatch ko na mismo ang 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,
});

At ina-apply ko rin ang parehong patch sa WebGL2RenderingContext kung meron.

Pagkatapos nito, nawala lahat ng shader compilation errors. Sa puntong ito, gumagana na si croxyproxy pero si proxyorb hindi pa rin. Bakit?! Hindi ba dapat pareho lang ang takbo nun?


Pass 2: Ang pangalawang problema (mga domain) at bakit mas gumaan ang buhay nang alisin ko ang foony.io

Historically, dalawang domain ang gamit ng Foony, hindi bababa sa nakaraang buwan:

  • foony.com para sa main site
  • foony.io para sa static assets

Praktikal ang orihinal na dahilan: kapag galing sa cookie-less domain ang mga asset, hindi kasamang naipapadala ang cookie header sa bawat static file request. Maganda ito dahil hindi lumalaki masyado ang upload ng headers. Pero hindi rin ito ganun kaimportante gaya ng iisipin mo, dahil gamit na ng HTTP/2 ang HPACK para paliitin ang bytes na pinapadala para sa headers.

Valid na optimization ito sa normal na browsing.

Pero sa likod ng mga proxy, naging malaking pinagmumulan ito ng mga sira. At sobrang hilig ng mga user ng Foony sa mga proxy. sigh

Iba ang trato ng mga proxy sa “main site” kumpara sa “ibang site”

Maraming proxy ang naka-optimize sa “i-proxy ang page / domain na ito lang.” Kaya nilang i-load ang main HTML nang maayos, mag-inject ng scripts, mag-register ng sarili nilang ServiceWorker, at iba pa.

Pero pag nagsimula nang humila ng assets ang app mula sa ibang origin (tulad ng foony.io), dito na pumapasok ang samu’t saring sabit:

  • Mga failure sa ServiceWorker interception gaya ng:
    • “ServiceWorker intercepted the request and encountered an unexpected error”
    • “Loading failed for the module with source”
  • Pagiging required ng mga query params ng proxy infrastructure (na madali ring hindi sinasadyang matanggal).
  • Mga asset request na nawawalan ng internal routing metadata ng proxy.
  • Mga weird na proxy na pinapalitan ang buong request ng generic-php-slug.php?someQueryParam=hugeEncodedString (oo, hindi ko na sinubukang suportahan ‘yan).

Nakadepende ang mga internal na mekanismo ng proxy sa query params / URL encoding, at sobrang fragile nila.

Isa sa mga halatang halimbawa ay yung asset URL na ganito:

https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20

Yung ?__pot=... ay sariling routing/state ng proxy, na nagsasabi sa proxy kung aling domain talaga para kanino ang request. Kapag tinanggal mo iyon, hindi na ma-resolve nang tama ng proxy ang request, at mauuwi ka sa error path ng ServiceWorker.

“Resource swapping” sa rescue (at bakit agad din kumomplikado)

Umabot ako sa puntong sinubukan kong gumawa ng workaround: kapag nadetect na “nasa proxy tayo”, papalitan ko ang lahat ng foony.io resource URL papunta sa kasalukuyang origin para ma-perceive ng proxy na same-origin lang lahat.

Mukhang may sense, at gumana siya sa croxyproxy, pero nagdala siya ng sobrang daming complexity:

  • Kailangan mong palitan ang mga link at script tag na nandyan na sa HTML.
  • Kailangan mo ng MutationObserver para sa mga dynamic na na-i-inject na tag (modulepreload, stylesheet, atbp.).
  • Kailangan mong i-preserve ang mga query parameter ng proxy, kung hindi masisira ang routing nila. At magkaiba ang paraan ng bawat proxy. Syempre naman.
  • At kailangan mo pa ring panatilihin na generic ang logic (walang proxy-specific globals) para hindi maging bloated na tambakan ng hacks ang code.

Dito rin ulit pumasok ang “base64 trick”: kahit sa sarili kong JavaScript, kailangan kong mag-ingat sa literal na string na location dahil puwedeng i-rewrite iyon ng proxy.

Pagre-reverse engineer sa injected script ng CroxyProxy

Dito na ako na-curious: ano bang talagang ginagawa ng proxy sa page ko? Nag-iinject ba sila ng sariling ads? O may mas malala pa?

Sobrang obfuscated ang client-side script ng CroxyProxy.

(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();

Na kapag pinatakbo, nagreresulta sa:

function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...

Base sa itsura, mukhang gumagamit ang croxyproxy ng Obfuscator.io para sa obfuscation na ‘to. Buti na lang medyo madali na itong i-deobfuscate gamit ang webcrack.

Nagiging mas mababasa ito na 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)) {

Ayos. Kita na natin ang ginagawa niya. At… mukhang mostly okay naman. Sa tingin ko karamihan ng obfuscation ay para lang mahirapan silang madetect na proxy ito. Karamihan.

Meron silang ad / UI injection:

    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;
    }

May iba pang parte, pero sa madaling sabi naglalagay lang sila ng ads, kasama na yung mga pop-under style na ads. Gumagamit din sila ng FuckAdBlock.

Pero yung totoong string replacement, nangyayari iyon sa server-side. At kung ano pa ang ginagawa nila doon, sila lang ang nakakaalam.

Kaya kung mahalaga sa’yo ang security ng account mo, hindi mo dapat gamitin ang mga web proxy. Kung wala ka talagang choice, iwasang maglagay ng kahit anong PII / account / purchase information.

Tapon ang "resource swapping"

Napagdesisyunan ko na yung sobrang daming complexity dahil sa resource swapping, kasama ng iba pang kumplikado pang code para suportahan ang foony.io, ay hindi sulit kapalit ng kaunting network savings mula sa magagandang, cookieless requests. Nakakakita rin kami ng hindi maipaliwanag na pagbaba sa gameplay conversions simula nang gumamit kami ng foony.io, kaya malamang may iba pang problema sa foony.io na hindi namin alam.

Kaya tinanggal ko ang foony.io. At least sa ngayon.

Nang binura ko ang foony.io CDN logic at ginawang standard ang lahat sa foony.com, sobrang gumaan ang proxy support:

  • Same-origin na asset loads.
  • Mas kaunting “special case” na kailangan ipaliwanag sa isang proxy ServiceWorker.
  • Mas kaunting rewriting.
  • Mas hindi marupok na code.

Sa madaling sabi, ang pagtanggal sa foony.io ay isang architectural simplification na nagbawas sa surface area para sa kung anu-anong kabaliwan ng mga proxy.


Pass 3: Ano ang gumagana, ano ang hindi, at bakit

Mga proxy na sure na gumagana

Sa puntong ito, gumagana ang Foony sa likod ng:

  • croxyproxy
  • proxyorb

May iba pang proxy na malamang gumagana. Sigurado rin ako na karamihan hindi pa rin. Pero kahit papaano, yung mga madalas gamitin ng mga tao para maglaro, iyon ang gumagana na ngayon.

Bakit hindi “lahat ng proxy”?

May mga proxy talagang hindi kakayanin ang modern multiplayer web app. Halimbawa:

  • Mga proxy na hindi maayos ang suporta sa HTTPS.
  • Mga proxy na binablock o sinisira ang WebSockets (gamit ng Foony ang real-time networking). Technically puwede itong i-work around, pero sobrang dodoble ang complexity.
  • Mga proxy na sobrang higpit sa cross-origin requests, headers, o ServiceWorkers.

Mga pangunahing aral

Mga web proxy ay sobrang hindi ligtas

Middleware sila na:

  • nagre-rewrite ng HTML
  • nagre-rewrite ng JavaScript
  • minsan nag-iinject pa ng ServiceWorker
  • at madalas nakadepende sa query params / URL encoding para i-route ang requests
  • at puwedeng kung anu-ano ang gawin sa mga page mo

Nagulat din ako kung gaano kalalim ang kaya gawin ng ibang proxy: nirerewrite nila ang shader source strings, comments, at kung ano-ano pa na hindi na natin alam.

Minsan ang pinakamagandang fix ay architectural

Pinagana ulit ng WebGL patch ang pag-render ng mga laro, pero ang pagtanggal sa multi-domain CDN strategy ang nagpa-stable sa proxy support sa katagalan.

Paalala rin ito: minsan okay naman ang mga clever optimization, hanggang sa sumalpok sila sa hostile na middleware. O sa browser extension ng user. O sa Safari. O sa language settings. O sa accessibility features. O sa solar flares. O sa kung ano pa man.


Conclusion

Gumagana na ngayon ang Foony sa likod ng mga proxy na may pakialam kami (croxyproxy at proxyorb), nang hindi ginagawang proxy-specific na gulo ang buong codebase:

  • Isang generic na Three.js shader fix (walang proxy-specific identifiers).
  • Isang mas simpleng domain strategy (foony.com saanman).
8 Ball Pool online multiplayer billiards icon