background blurbackground mobile blur

1/1/1970

Bagaimana Aku Membuat Foony Bisa Jalan di Balik Proxy

Halo! Aku sudah lama tahu kalau web proxy bisa bikin masalah kompatibilitas untuk website. Tapi dukungan Foony saat dipakai lewat proxy parah banget, dan memperbaiki kompatibilitas Foony dengan proxy lumayan tricky.

Ini juga bukan masalah macam “Foony pakai API aneh-aneh” (walaupun kami memang pakai). Ini lebih ke kombinasi dari:

  • Proxy yang terlalu agresif mengubah string di tempat-tempat yang seharusnya sama sekali nggak disentuh.
  • Proxy yang memperlakukan domain situs utama beda dengan domain “lainnya” (CDN, host aset, dan sebagainya).
  • Dan kenyataan pahit bahwa beberapa proxy memang nggak mampu mendukung web app modern (HTTPS yang benar, WebSockets, dan lain-lain).

Foony belum bisa jalan di semua proxy, tapi sekarang minimal sudah jalan di croxyproxy dan proxyorb, dan itu memang target awalnya.

Di bawah ini aku jelasin apa saja yang rusak, kenapa bisa rusak, dan perbaikan mana yang ternyata benar-benar penting.


Pass 1: Shader Three.js yang valid tapi tetap rusak

Gejalanya

Waktu aku coba croxyproxy, aku nggak bisa main 8 Ball Pool atau game Foony lainnya yang pakai three.js juga. Aku terus-menerus dapat error kompilasi shader di Three.js dengan pesan seperti:

  • “Shader Error 1282 - VALIDATE_STATUS false”

Pesan itu hampir nggak ada gunanya. Biasanya artinya cuma “shader kamu nggak valid, selamat berjuang.” Mantap. Kalau kamu pernah penasaran kenapa aku selalu pakai pesan error yang unik untuk setiap error di Foony, ya ini alasannya. Itu bantu banget buat nunjuk masalahnya, bukan cuma "kodenya rusak, silakan beresin sendiri."

Terus kenapa shader three.js yang sebenarnya valid bisa rusak? Ada apa nih?

Penyebab sebenarnya: proxy merusak layout(location = N)

Three.js mengeluarkan kode GLSL dengan layout qualifier seperti:

layout(location = 0) in vec3 position;

Beberapa proxy mencoba mengubah apa pun yang mirip dengan API JavaScript location dengan cara replace string global yang super naif. Di JavaScript saja itu sudah buruk, tapi mereka juga melakukannya di dalam string source shader. Kayaknya parsing AST terlalu mahal buat mereka.

Akibatnya source shader jadi korup dan berubah jadi seperti:

layout(__cpLocation = 0) in vec3 position;

Identifier di situ harus location. Apa pun selain itu dianggap GLSL yang nggak valid, dan compiler bakal menolaknya. (Layout Qualifiers in GLSL)

Ini cuma bisa dibilang “masalah Three.js” dalam arti Three.js menghasilkan shader secara dinamis, dan kita kirim ke WebGL waktu runtime. Bug yang sebenarnya ada di cara proxy melakukan rewriting.

Kenapa aku nggak “memperbaiki proxynya” saja

Pendekatan polosnya adalah mencari string pengganti location milik croxyproxy, yaitu __cpLocation, lalu menggantinya lagi jadi location. Tapi tiap proxy pakai nama pengganti yang beda-beda. Ada yang pakai __cpLocation, ada juga yang pakai identifier aneh lain. Jadi nge-hardcode perbaikan seperti “ganti __cpLocation balik ke location” itu rapuh banget.

Aku butuh:

  • Perbaikan yang generik (tanpa hardcode identifier proxy).
  • Perbaikan yang tetap jalan walaupun proxy ikut mengubah kata location di JavaScript-ku.

Trik base64: nyembunyiin kata location dari proxy

Kalau proxynya mengubah setiap location yang kelihatan, langkah paling gampang ya tinggal jangan pakai kata location secara langsung. Gampang kan. Aku pernah lihat trik kayak gini sebelumnya di Lua waktu aku reverse-engineer sistem proteksi guide RestedXP (kalau nggak salah ingat, mereka mengaburkan pemakaian BNGetInfo, misalnya _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Trik ini tentu saja juga bisa dipakai di JavaScript. Di client/index.html, aku decode string berikut saat runtime:

// 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

Pemanggilan atob() itu terjadi setelah proxy selesai melakukan rewriting HTML/JS, jadi dia nggak bisa “mengkorupsi duluan” stringnya. Aku pecah stringnya jadi dua bagian supaya makin susah dideteksi, dan aku pakai 'atob' karena ya kenapa nggak, tapi String.fromCharCode atau hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] juga seharusnya jalan.

Pola shader yang rusak itu selalu punya struktur yang sama:

layout(<something> = <number>)

Jadi aku cocokkan pola itu secara generik dan mengganti <something> dengan identifier yang benar:

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

Hook WebGL: patch shaderSource (WebGL1 + WebGL2)

Karena Three.js memanggil gl.shaderSource(shader, source), aku patch fungsi shaderSource itu sendiri:

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

Dan aku terapkan patch yang sama ke WebGL2RenderingContext kalau objek itu ada.

Begitu itu dipasang, error kompilasi shader langsung hilang. Di titik ini, croxyproxy sudah jalan, tapi proxyorb masih gagal. Kok bisa?! Harusnya kan cara kerjanya mirip.


Pass 2: Masalah kedua (domain) dan kenapa menghapus foony.io bikin semuanya lebih gampang

Secara historis, setidaknya selama sebulan terakhir, Foony pakai dua domain:

  • foony.com untuk situs utama
  • foony.io untuk aset statis

Alasan awalnya cukup praktis: melayani aset dari domain tanpa cookie menghindari beban header cookie yang ikut terkirim di setiap request file statis. Ini bagus, tapi sebenarnya nggak sewajib yang mungkin kamu bayangkan, apalagi HTTP/2 sudah pakai HPACK buat mengurangi jumlah byte header yang dikirim.

Itu optimasi yang masuk akal di situasi browsing normal.

Begitu lewat proxy, optimasi itu malah jadi sumber masalah besar. Dan pengguna Foony itu doyan banget pakai proxy. sigh

Proxy memperlakukan “situs utama” beda dengan “situs lain”

Banyak proxy yang dioptimalkan untuk “proxy satu halaman / satu domain ini saja”. Mereka bisa dengan mulus memuat HTML utama, menyuntikkan script mereka, mendaftarkan ServiceWorker mereka sendiri, dan sebagainya.

Tapi begitu aplikasi mulai mengambil aset dari origin lain (misalnya foony.io), mulai deh muncul berbagai macam kerusakan seru:

  • Kegagalan intercept di ServiceWorker, misalnya:
    • “ServiceWorker intercepted the request and encountered an unexpected error”
    • “Loading failed for the module with source”
  • Query param yang ternyata wajib buat infrastruktur proxy (dan gampang banget tanpa sengaja terhapus).
  • Request aset yang kehilangan metadata routing internal milik proxy.
  • Proxy aneh yang mengganti seluruh request jadi generic-php-slug.php?someQueryParam=hugeEncodedString (ya, yang ini nggak aku dukung).

Mekanisme internal proxy-proxy ini sangat bergantung pada query param / encoding URL, dan semuanya super rapuh.

Salah satu contoh yang cukup jelas adalah URL aset seperti:

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

?__pot=... itu adalah routing/state internal proxy yang ngasih tahu proxynya, request ini sebenarnya buat domain mana. Kalau param itu hilang, proxy nggak bisa resolve request dengan benar, dan kamu akan berakhir di jalur error ServiceWorker.

“Resource swapping” datang menyelamatkan (dan kenapa cepat banget jadi rumit)

Pernah di satu titik aku coba trik workaround: deteksi dulu “kita lagi di belakang proxy”, lalu menukar semua URL resource foony.io ke origin saat ini supaya dari sudut pandang proxy semuanya kelihatan same-origin.

Kedengarannya masuk akal, dan ini sempat berhasil di croxyproxy, tapi efek sampingnya nambahin banyak kerumitan:

  • Kamu harus mengganti tag link dan script yang sudah ada di HTML.
  • Kamu butuh MutationObserver untuk menangani tag yang disuntikkan secara dinamis (modulepreload, stylesheet, dan lain-lain).
  • Kamu harus mempertahankan query parameter milik proxy, kalau tidak routing mereka bakal rusak. Dan tiap proxy melakukan ini dengan cara berbeda. Tentu saja.
  • Dan kamu tetap harus menjaga logikanya tetap generik (tanpa global khusus per proxy) supaya kode tidak berubah jadi tumpukan sampah raksasa.

Di sini juga trik “base64” tadi kepakai lagi: bahkan di JavaScript-ku sendiri aku harus hati-hati dengan string literal location karena bisa saja proxynya mengubah itu.

Reverse-engineering script yang disuntikkan CroxyProxy

Di titik ini aku jadi kepo: sebenarnya proxy ini ngapain ke halaman aku? Cuma nyuntik iklan? Atau ada yang lebih parah?

Script sisi klien milik CroxyProxy di-obfuscate berat.

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

Yang kalau dijalankan, hasilnya jadi seperti ini:

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

Dari sini kelihatan kalau croxyproxy pakai Obfuscator.io buat obfuscation ini. Untungnya ini cukup gampang di-deobfuscate dengan webcrack.

Hasilnya, JavaScript-nya jadi jauh lebih gampang dibaca:

((_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)) {

Asik. Sekarang kita bisa lihat dia ngapain. Dan... kelihatannya sih sebagian besar baik-baik saja. Kayaknya obfuscation ini lebih ke buat nyulitkan deteksi keberadaan proxy. Kebanyakan sih begitu.

Ada beberapa bagian yang nyuntik iklan / 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;
    }

Masih ada beberapa bagian lain, tapi intinya sih dia cuma nampilin iklan, termasuk iklan model pop-under. Dia juga pakai FuckAdBlock.

Tapi penggantian string yang “serius” itu sebenarnya terjadi di sisi server. Dan siapa yang tahu apa saja yang mereka lakukan di sana.

Apa pun ceritanya, kamu sebaiknya tidak pakai web proxy kalau kamu peduli dengan keamanan akunmu. Kalau terpaksa pakai, jangan pernah masukin data pribadi / data akun / info pembayaran apa pun.

“Resource swapping” masuk tong sampah

Akhirnya aku putuskan kalau kerumitan dari resource swapping, ditambah kerumitan di bagian kode lain demi mendukung foony.io, nggak sebanding dengan sedikit penghematan network dari request cantik tanpa cookie. Kami juga melihat penurunan konversi gameplay yang nggak jelas sebabnya sejak mulai pakai foony.io, jadi aku curiga ada masalah lain dengan foony.io yang belum kami sadari.

Jadi aku hapus saja foony.io. Setidaknya untuk sekarang.

Begitu aku hapus semua logika CDN foony.io dan menstandarkan semuanya ke foony.com, dukungan untuk proxy jadi jauh lebih sederhana:

  • Asset selalu dimuat dari origin yang sama.
  • Lebih sedikit “kasus khusus” yang harus dijelaskan ke ServiceWorker milik proxy.
  • Lebih sedikit rewriting.
  • Kode jadi lebih tidak rapuh.

Singkatnya, menghapus foony.io adalah penyederhanaan arsitektur yang mengurangi area permukaan buat tingkah aneh proxy.


Pass 3: Apa yang jalan, apa yang nggak, dan kenapa

Proxy yang sudah dipastikan jalan

Di titik ini, Foony bisa jalan di belakang:

  • croxyproxy
  • proxyorb

Mungkin ada beberapa proxy lain yang juga jalan. Tapi aku yakin sebagian besar masih belum. Setidaknya proxy penting yang paling sering dipakai orang buat main game kelihatannya sudah beres.

Kenapa nggak “semua proxy” saja?

Beberapa proxy memang sama sekali nggak mampu mendukung web app multiplayer modern. Contohnya:

  • Proxy yang dukungan HTTPS-nya berantakan.
  • Proxy yang merusak atau memblokir WebSocket (Foony pakai jaringan real-time). Secara teori ini bisa diakali, tapi bakal nambah kerumitan.
  • Proxy yang punya terlalu banyak batasan soal cross-origin request, header, atau ServiceWorker.

Hal-hal utama yang kupelajari

Web proxy itu sangat tidak aman

Mereka adalah middleware yang:

  • mengubah ulang HTML
  • mengubah ulang JavaScript
  • kadang menyuntikkan ServiceWorker
  • dan sering bergantung pada query param / encoding URL untuk merutekan request
  • bisa macam-macam utak-atik halamanmu sesuka hati

Aku cukup kaget melihat seberapa dalam beberapa proxy ini masuk: mereka mengubah string source shader, komentar, dan entah apa lagi.

Kadang perbaikan terbaik itu ada di level arsitektur

Patch WebGL bikin game bisa render lagi, tapi menghapus strategi CDN multi-domain yang bikin dukungan proxy jadi tetap stabil.

Ini pengingat yang bagus: optimasi pintar bisa saja terasa masuk akal, sampai mereka bertabrakan dengan middleware yang “bermusuhan”. Atau extension browser pengguna. Atau Safari. Atau pengaturan bahasa. Atau fitur aksesibilitas. Atau semburan matahari. Atau apa saja, sebenarnya.


Penutup

Sekarang Foony bisa jalan di balik proxy-proxy yang penting (croxyproxy dan proxyorb), tanpa membuat codebase jadi berantakan penuh if khusus proxy:

  • Perbaikan shader Three.js yang generik (tanpa identifier khusus proxy).
  • Strategi domain yang lebih sederhana (foony.com di semua tempat).
8 Ball Pool online multiplayer billiards icon