

1/1/1970
Bagaimana Saya Membuat Foony Bekerja di Balik Proxy
Halo! Sudah lama saya tahu bahwa proxy web menyebabkan masalah kompatibilitas pada situs web. Namun, dukungan Foony di proxy terkenal buruk, dan menyelesaikan kompatibilitas proxy Foony cukup rumit.
Ini juga bukan masalah "Foony menggunakan API yang eksotis" (meskipun memang begitu). Ini adalah kombinasi dari:
- Proxy yang melakukan penulisan ulang string secara agresif di tempat-tempat yang seharusnya tidak boleh.
- Proxy memperlakukan domain situs utama berbeda dari domain "lainnya" (CDN, host aset, dll.).
- Dan kenyataan pahit bahwa beberapa proxy memang tidak bisa mendukung aplikasi web modern (kebenaran HTTPS, WebSockets, dll.).
Kami tidak bekerja dengan setiap proxy, tapi sekarang kami setidaknya berfungsi dengan croxyproxy dan proxyorb, yang memang menjadi tujuan.
Di bawah ini saya jelaskan apa yang rusak, mengapa rusak, dan perbaikan yang benar-benar penting.
Tahap 1: Shader Three.js yang valid tapi rusak
Gejalanya
Saat saya mencoba croxyproxy, saya tidak bisa memainkan 8 Ball Pool atau game three.js lainnya milik Foony. Saya terus mendapatkan kegagalan kompilasi shader di Three.js dengan error seperti:
- "Shader Error 1282 - VALIDATE_STATUS false"
Pesan itu hampir sama sekali tidak berguna. Biasanya artinya "shader Anda tidak valid, semoga beruntung." Hebat. Kalau Anda pernah bertanya-tanya kenapa saya selalu menggunakan pesan error yang unik untuk setiap error di Foony, inilah alasannya. Ini membantu menentukan masalah daripada hanya "kodenya rusak, perbaiki sana."
Tapi kenapa shader three.js yang sangat valid bisa rusak? Ada apa ini?
Penyebab sebenarnya: proxy merusak layout(location = N)
Three.js mengeluarkan GLSL dengan layout qualifier seperti:
layout(location = 0) in vec3 position;
Beberapa proxy mencoba menulis ulang apa pun yang terlihat seperti API JavaScript location dengan melakukan penggantian string global yang naif. Itu sudah buruk di JS, tapi mereka juga melakukannya di dalam string source shader. Saya rasa parsing AST terlalu mahal bagi mereka.
Jadi source shader rusak menjadi seperti ini:
layout(__cpLocation = 0) in vec3 position;
Identifier di sana harus location. Selain itu adalah GLSL yang tidak valid, dan compiler akan menolaknya. (Layout Qualifiers in GLSL)
Ini adalah masalah Three.js hanya dalam arti Three.js menghasilkan shader secara dinamis, dan kami meneruskannya ke WebGL pada runtime. Bug sebenarnya ada pada strategi penulisan ulang proxy.
Mengapa saya tidak "memperbaiki proxy"
Pendekatan naif adalah mencari string pengganti location milik croxyproxy, yaitu __cpLocation, dan menggantinya dengan location. Namun, proxy yang berbeda menggunakan nama pengganti yang berbeda. Ada yang menggunakan __cpLocation, ada yang menggunakan identifier aneh lainnya. Jadi memberi kode keras seperti "ganti __cpLocation kembali ke location" sangat rapuh.
Saya butuh:
- Perbaikan yang generik (tanpa kode keras identifier proxy).
- Perbaikan yang berfungsi bahkan jika proxy juga menulis ulang kata
locationdi JavaScript saya.
Trik base64: menyembunyikan kata location dari proxy
Jika proxy menulis ulang setiap literal location yang dilihatnya, langkah paling sederhana adalah dengan tidak menggunakan location. Cukup mudah. Saya pernah melihat trik seperti ini di Lua saat membongkar sistem proteksi panduan RestedXP (kalau saya tidak salah ingat, mereka mengaburkan penggunaan BNGetInfo, misalnya _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Trik ini juga berfungsi di JavaScript, tentu saja. Di client/index.html, saya mendekode hal berikut pada 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
atob() itu terjadi setelah proxy sudah melakukan penulisan ulang HTML/JS, jadi tidak bisa "merusak" string itu lebih awal. Saya membagi string menjadi dua agar lebih sulit dideteksi, dan saya menggunakan 'atob' karena saya bisa, tapi String.fromCharCode atau hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] mungkin juga berfungsi.
Pola shader yang rusak selalu sama secara struktural:
layout(<something> = <number>)
Jadi saya cocokkan secara generik dan ganti <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), saya patch 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 saya menerapkan patch yang sama ke WebGL2RenderingContext jika tersedia.
Setelah itu diterapkan, error kompilasi shader hilang. Saat itu, croxyproxy sudah berfungsi tapi proxyorb masih gagal. Kenapa?! Bukankah seharusnya bekerja dengan cara yang sama?
Tahap 2: Masalah kedua (domain) dan kenapa menghapus foony.io mempermudah segalanya
Foony secara historis menggunakan dua domain, setidaknya selama sebulan terakhir:
foony.comuntuk situs utamafoony.iountuk aset statis
Alasan awalnya praktis: melayani aset dari domain tanpa cookie menghindari pembengkakan upload header cookie pada setiap permintaan file statis. Itu bagus, tapi tidak seperlu yang Anda kira mengingat HTTP/2 menggunakan HPACK untuk mengurangi byte yang dikirim untuk header.
Itu adalah optimasi yang valid dalam penjelajahan normal.
Di balik proxy, itu menjadi sumber utama kerusakan. Dan basis pengguna Foony sangat suka proxy. menghela nafas
Proxy memperlakukan "situs utama" berbeda dari "situs lain"
Banyak proxy dioptimalkan untuk "proxy halaman / domain ini saja." Mereka berhasil memuat HTML utama, menyuntik script, mendaftarkan ServiceWorker mereka sendiri, dll.
Tapi ketika aplikasi mulai mengambil aset dari origin yang berbeda (seperti foony.io), Anda akan masuk ke berbagai macam kerusakan menyenangkan:
- Kegagalan intersepsi ServiceWorker seperti:
- "ServiceWorker intercepted the request and encountered an unexpected error"
- "Loading failed for the module with source"
- Query param yang dibutuhkan oleh infrastruktur proxy (dan mudah secara tak sengaja dihapus).
- Permintaan aset kehilangan metadata routing internal proxy.
- Proxy aneh yang mengganti seluruh permintaan dengan
generic-php-slug.php?someQueryParam=hugeEncodedString(ya, saya tidak repot-repot mendukung itu).
Mekanisme internal proxy ini bergantung pada query param / encoding URL, dan cukup rapuh.
Salah satu contoh yang paling jelas adalah URL aset seperti:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
?__pot=... itu adalah routing/state milik proxy yang memberi tahu proxy domain mana yang dituju permintaan tersebut. Jika Anda menghapusnya, proxy tidak bisa menyelesaikan permintaan dengan benar, dan Anda berakhir dengan jalur error ServiceWorker.
"Resource swapping" untuk menyelamatkan (dan kenapa cepat menjadi rumit)
Pada satu titik, saya mencoba solusi: deteksi "kita sedang di-proxy," kemudian tukar URL resource foony.io ke origin saat ini sehingga proxy melihat semuanya sebagai same-origin.
Itu terdengar masuk akal, dan berhasil untuk croxyproxy, tapi menambahkan banyak kompleksitas:
- Anda perlu mengganti tag
linkdanscriptyang sudah ada di HTML. - Anda butuh
MutationObserveruntuk menangani tag yang disuntikkan secara dinamis (modulepreload, stylesheet, dll.). - Anda harus mempertahankan query parameter proxy, atau Anda merusak routing mereka. Dan proxy yang berbeda melakukannya dengan cara yang berbeda. Karena tentu saja begitu.
- Dan Anda masih harus membuat logika tetap generik (tanpa global khusus proxy) supaya kode tidak menjadi tempat sampah yang membengkak.
Di sinilah "trik base64" muncul lagi: bahkan di JavaScript saya sendiri, saya harus berhati-hati dengan literal string location karena proxy mungkin menulis ulangnya.
Membongkar script yang disuntikkan CroxyProxy
Pada titik ini saya jadi penasaran: apa sebenarnya yang dilakukan proxy pada halaman saya? Apakah menyuntikkan iklannya sendiri? Sesuatu yang lebih buruk?
Script sisi klien CroxyProxy sangat dikaburkan.
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
Yang ketika dijalankan, menghasilkan:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
Berdasarkan ini, sepertinya croxyproxy menggunakan Obfuscator.io untuk pengaburan ini. Untungnya cukup mudah untuk di-deobfuscate dengan webcrack.
Hasilnya adalah JavaScript yang jauh lebih mudah 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)) {
Bagus. Sekarang kita bisa lihat apa yang dilakukannya. Dan... sebagian besar terlihat baik-baik saja. Saya rasa pengaburan itu sebagian besar untuk membantu mencegah deteksi proxy. Sebagian besar.
Kita punya beberapa injeksi 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;
}
Ada beberapa tempat lain, tapi pada dasarnya hanya menampilkan iklan, termasuk iklan gaya pop-under. Itu juga menggunakan FuckAdBlock.
Penggantian string yang sebenarnya, bagaimanapun, terjadi di sisi server. Dan siapa yang tahu apa saja yang dilakukannya.
Bagaimanapun, Anda benar-benar tidak boleh menggunakan proxy web jika Anda peduli dengan keamanan akun Anda. Jika harus, hindari memasukkan informasi PII / akun / pembelian apa pun.
"Resource swapping" dibuang ke tong sampah
Saya memutuskan bahwa kompleksitas dari resource swapping, ditambah dengan kompleksitas di bagian kode lain untuk dukungan foony.io, tidak sebanding dengan penghematan jaringan kecil dari permintaan tanpa cookie yang indah. Kami juga melihat penurunan yang tidak dapat dijelaskan dalam konversi gameplay sejak mengadopsi foony.io, jadi saya curiga ada masalah lain dengan foony.io yang tidak kami sadari.
Jadi saya menghapus foony.io. Setidaknya untuk sekarang.
Setelah saya menghapus logika CDN foony.io dan menstandarisasi semuanya pada foony.com, dukungan proxy menjadi jauh lebih sederhana:
- Pemuatan aset same-origin.
- Lebih sedikit "kasus khusus" untuk dijelaskan ke ServiceWorker proxy.
- Lebih sedikit penulisan ulang.
- Kode yang kurang rapuh.
Singkatnya, menghapus foony.io adalah penyederhanaan arsitektur yang mengurangi area permukaan untuk perilaku proxy yang aneh.
Tahap 3: Apa yang berfungsi, apa yang tidak, dan kenapa
Proxy yang dikonfirmasi berfungsi
Pada titik ini, Foony berfungsi di balik:
- croxyproxy
- proxyorb
Beberapa proxy lain mungkin berfungsi. Saya yakin sebagian besar masih tidak berfungsi. Tapi setidaknya yang penting yang dipakai orang untuk bermain game tampaknya berfungsi.
Kenapa tidak "semua proxy"?
Beberapa proxy memang tidak bisa mendukung aplikasi web multiplayer modern. Contohnya:
- Proxy yang tidak mendukung HTTPS dengan benar.
- Proxy yang merusak atau memblokir WebSockets (Foony menggunakan jaringan real-time). Secara teknis Anda bisa mengakalinya, tapi itu akan menambah kompleksitas.
- Proxy yang memiliki terlalu banyak pembatasan terkait permintaan cross-origin, header, atau ServiceWorker.
Poin penting
Proxy web sangat tidak aman
Mereka adalah middleware yang:
- menulis ulang HTML
- menulis ulang JavaScript
- kadang menyuntikkan ServiceWorker
- dan sering bergantung pada query param / encoding URL untuk merutekan permintaan
- bisa mengutak-atik halaman Anda dengan berbagai cara
Saya terkejut betapa dalamnya beberapa proxy: mereka akan menulis ulang string source shader, komentar, dan entah apa lagi.
Kadang perbaikan terbaik adalah arsitektural
Patch WebGL membuat game berjalan lagi, tapi menghapus strategi CDN multi-domain membuat dukungan proxy tetap stabil.
Itu pengingat bagus: optimasi cerdas bisa terlihat sangat masuk akal sampai mereka bertabrakan dengan middleware yang bermusuhan. Atau ekstensi browser pengguna. Atau Safari. Atau pengaturan bahasa. Atau fitur aksesibilitas. Atau jilatan matahari. Atau apa pun, sungguh.
Kesimpulan
Foony sekarang berfungsi di balik proxy yang penting (croxyproxy dan proxyorb), tanpa membuat codebase menjadi kekacauan khusus proxy:
- Perbaikan shader Three.js yang generik (tanpa identifier khusus proxy).
- Strategi domain yang lebih sederhana (foony.com di mana-mana).