

1/1/1970
मैंने Foony को प्रॉक्सी के पीछे कैसे चलाया
नमस्ते! मुझे काफ़ी समय से पता है कि वेब प्रॉक्सी अक्सर वेबसाइटों के लिए कम्पेटिबिलिटी की दिक्कतें पैदा करती हैं। लेकिन प्रॉक्सी के पीछे Foony का सपोर्ट बदनाम रूप से खराब रहा है, और Foony की प्रॉक्सी कम्पेटिबिलिटी सुलझाना काफ़ी पेचीदा निकला।
ये कोई “Foony uses exotic APIs” वाला मसला भी नहीं था (हालाँकि हम वाक़ई ऐसा करते हैं)। असल में ये कई चीज़ों के मेल का नतीजा था:
- प्रॉक्सी कुछ ऐसी जगहों पर भी स्ट्रिंग को बहुत आक्रामक तरीके से बदल रही थीं जहाँ इन्हें बिल्कुल नहीं छूना चाहिए था।
- प्रॉक्सी main साइट डोमेन को “बाकी” डोमेन्स (CDN, asset hosts वगैरह) से अलग तरह से ट्रीट कर रही थीं।
- और ये सख़्त सच्चाई कि कुछ प्रॉक्सी सीधे‑सीधे मॉडर्न वेब ऐप्स को सपोर्ट ही नहीं कर सकतीं (HTTPS correctness, WebSockets वगैरह)।
हर प्रॉक्सी पर हमारा काम नहीं चलता, लेकिन अब कम से कम croxyproxy और proxyorb पर तो सब चलता है, और मेरा टार्गेट भी यही था।
नीचे मैं बता रहा हूँ कि क्या टूटा, क्यों टूटा, और कौन से फ़िक्स सच में मायने रखते थे।
पास 1: वैध, फिर भी टूटे हुए Three.js shaders
लक्षण
जब मैंने croxyproxy ट्राई की, मैं 8 Ball Pool या Foony की बाकी three.js games बिल्कुल नहीं खेल पा रहा था। हर बार Three.js में shader compilation फेल हो जाती थी, और ऐसे errors दिखते थे:
- “Shader Error 1282 - VALIDATE_STATUS false”
ये मैसेज लगभग बेकार था। आम तौर पर इसका मतलब बस इतना होता है कि “तुम्हारा shader invalid है, अब खुद देख लो।” अच्छा, धन्यवाद। अगर तुम्हें कभी ये सोचने का मन हो कि मैं Foony पर हर एक एरर के लिए अलग, यूनिक एरर मैसेज क्यों रखता हूँ, तो वजह यही है। इससे ये पता लगाने में मदद मिलती है कि दिक्कत कहाँ है, बजाय इसके कि बस “कोड टूटा है, जाओ ठीक करो।”
लेकिन जो three.js shaders बिल्कुल वैध थे, वे टूट क्यों रहे थे? हो क्या रहा था?
असल वजह: प्रॉक्सी का layout(location = N) को बिगाड़ देना
Three.js जो GLSL निकालता है, उसमें ऐसे layout qualifiers होते हैं:
layout(location = 0) in vec3 position;
कुछ प्रॉक्सी हर उस चीज़ को बदलने की कोशिश करती हैं जो JavaScript के location API जैसा दिखे, और ये काम वो बहुत ही सीधी-सादी global string replace से करती हैं। JS में भी ये काफ़ी बुरा है, पर ये लोग वही काम shader source strings के अंदर भी कर रहे थे। शायद इनके लिए AST parsing बहुत महँगी चीज़ है।
तो shader source कुछ ऐसा बिगड़ गया:
layout(__cpLocation = 0) in vec3 position;
वहाँ identifier ज़रूर location ही होना चाहिए। कुछ भी और होगा तो वो invalid GLSL है, और compiler उसे reject कर देगा। (Layout Qualifiers in GLSL)
ये Three.js की समस्या सिर्फ़ इस मायने में है कि Three.js shaders को dynamically बनाता है, और हम उन्हें runtime पर WebGL को देते हैं। असली बग प्रॉक्सी की rewriting strategy में है।
मैंने “प्रॉक्सी को ही ठीक” क्यों नहीं किया
सबसे सीधा दिखने वाला तरीका ये होता कि croxyproxy के location replacement string, __cpLocation, को ढूँढकर वापस location से बदल दें। लेकिन अलग-अलग प्रॉक्सी अलग-अलग replacement names इस्तेमाल करती हैं। कोई __cpLocation लगाती है, कोई और अजीब identifier। तो ऐसा हार्डकोडेड फ़िक्स कि “__cpLocation को फिर से location बना दो” बहुत नाज़ुक है।
मुझे चाहिए था:
- एक generic फ़िक्स (कोई proxy-specific identifier हार्डकोड नहीं)।
- ऐसा फ़िक्स जो तब भी चले जब प्रॉक्सी मेरे JavaScript में भी
locationशब्द को rewrite कर रही हो।
base64 ट्रिक: प्रॉक्सी से location शब्द को छिपाना
अगर प्रॉक्सी हर literal location को rewrite कर देती है, तो सबसे आसान चाल है कि हम सीधा-सीधा location शब्द इस्तेमाल ही न करें। आसान उपाय। मैंने ये ट्रिक पहले Lua में देखी थी जब मैंने RestedXP के guide protection सिस्टम को reverse‑engineer किया था (अगर सही याद है तो वो लोग BNGetInfo की usage को इस तरह obfuscate करते हैं, जैसे _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F"))।
ये ट्रिक JavaScript में भी बिल्कुल काम करती है। client/index.html में मैं runtime पर ये decode करता हूँ:
// 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() तब चलता है जब प्रॉक्सी पहले ही अपना HTML/JS rewriting कर चुकी होती है, तो वो इस string को पहले से "corrupt" नहीं कर सकती। मैंने string को दो हिस्सों में बाँटा ताकि detect करना और मुश्किल हो जाए, और मैंने 'atob' का इस्तेमाल सिर्फ़ इसलिए किया क्योंकि कर सकता था, नहीं तो String.fromCharCode या hex-escaping जैसा window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] भी काम कर सकता था।
टूटा हुआ shader pattern structure के हिसाब से हमेशा ऐसा ही दिखता था:
layout(<something> = <number>)
तो मैंने इसे generic तरीके से match किया और <something> को सही identifier से बदल दिया:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
WebGL hook: shaderSource को patch करना (WebGL1 + WebGL2)
क्योंकि Three.js gl.shaderSource(shader, source) को कॉल करता है, मैंने सीधे shaderSource को ही patch कर दिया:
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,
});
और अगर WebGL2RenderingContext मौजूद है तो मैं वही patch उस पर भी लगाता हूँ।
ये लगाने के बाद shader compilation errors गायब हो गए। इस पॉइंट पर croxyproxy पर तो सब चल रहा था, लेकिन proxyorb अब भी टूट रहा था। क्यों?! ये दोनों पर एक जैसा नहीं चलना चाहिए था?
पास 2: दूसरा मसला (domains) और क्यों foony.io हटाने से सब आसान हो गया
इतिहास में Foony कम से कम पिछले एक महीने से दो domains यूज़ कर रही थी:
- main साइट के लिए
foony.com - static assets के लिए
foony.io
असल वजह practical थी: assets को cookie-less domain से serve करने पर हर static फ़ाइल रिक्वेस्ट के साथ cookie headers upload होने वाला फालतू बाइट्स का बोझ नहीं रहता। ये अच्छा optimization है, लेकिन उतना ज़्यादा ज़रूरी नहीं जितना तुम सोचोगे, क्योंकि HTTP/2 headers के bytes कम करने के लिए HPACK इस्तेमाल करता है।
नॉर्मल browsing में ये optimization बिल्कुल वैध है।
प्रॉक्सी के पीछे आते ही ये टूट-फूट की बड़ी वजह बन गया। और Foony के users को तो प्रॉक्सी बहुत पसंद हैं। sigh
प्रॉक्सी “main साइट” को “बाकी साइट्स” से अलग ट्रीट करती हैं
कई प्रॉक्सी इस तरह optimized होती हैं कि “बस ये वाला page / domain proxy करो।” वो main HTML को ठीक से लोड कर लेती हैं, अपने scripts inject कर लेती हैं, अपना ServiceWorker register कर लेती हैं वगैरह।
लेकिन जैसे ही app किसी दूसरे origin (जैसे foony.io) से assets लाना शुरू करती है, तमाम तरह की दिलचस्प टूटी-फूटी चीज़ें होने लगती हैं:
- ServiceWorker interception failures, जैसे:
- “ServiceWorker intercepted the request and encountered an unexpected error”
- “Loading failed for the module with source”
- प्रॉक्सी इंफ़्रास्ट्रक्चर को कुछ खास query params चाहिए होते हैं (और उन्हें गलती से हटा देना बहुत आसान है)।
- asset requests से प्रॉक्सी का internal routing metadata गायब हो जाता है।
- कुछ अजीब सी प्रॉक्सी तो पूरे request को ही बदलकर
generic-php-slug.php?someQueryParam=hugeEncodedStringजैसा कुछ बना देती हैं (हाँ, उसे सपोर्ट करने की मैंने कोशिश तक नहीं की)।
इन प्रॉक्सी के internal मैकेनिज़्म query params / URL encoding पर टिका रहता है, और वो काफ़ी नाज़ुक होता है।
ऐसी ही एक साफ़ दिखाई देने वाली मिसाल ये asset URL थी:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
वो ?__pot=... प्रॉक्सी की अपनी routing/state है, जो प्रॉक्सी को बताती है कि ये request किस domain के लिए है। अगर तुम उसे हटा दो, तो प्रॉक्सी request को ठीक से resolve नहीं कर पाती, और तुम ServiceWorker वाले error path में पहुँच जाते हो।
“Resource swapping” से अस्थायी मदद (और कैसे वो जल्दी जटिल हो गया)
एक समय पर मैंने ये workaround आज़माया: “हम प्रॉक्सी के पीछे हैं” ये detect करो, फिर सारे foony.io resource URLs को current origin पर स्वैप कर दो ताकि प्रॉक्सी की नज़र में सब कुछ same-origin दिखे।
सुनने में ये ठीक लगता है, और croxyproxy पर ये काम भी कर रहा था, लेकिन इससे बहुत ज़्यादा complexity बढ़ गई:
- तुम्हें HTML में पहले से मौजूद सारे
linkऔरscripttags को बदलना पड़ता। - dynamically injected tags (modulepreload, stylesheet वगैरह) के लिए
MutationObserverलगाना पड़ता। - प्रॉक्सी के query parameters बचाकर रखने पड़ते, वरना उनकी routing टूट जाती। और अलग-अलग प्रॉक्सी ये काम अलग तरीके से करती हैं। ज़ाहिर है।
- और फिर भी तुम्हें ये logic generic ही रखना होता (कोई proxy-specific globals नहीं), ताकि कोड proxy-specific कचरे का ढेर न बन जाए।
यहीं पर “base64 ट्रिक” दोबारा काम आई: अपने ही JavaScript में भी मुझे literal location string के साथ सावधान रहना पड़ रहा था, क्योंकि प्रॉक्सी उसे भी rewrite कर सकती थी।
CroxyProxy के injected script को reverse‑engineer करना
इस पॉइंट पर मुझे जिज्ञासा हुई: ये प्रॉक्सी मेरी page के साथ असल में कर क्या रही है? क्या ये बस अपने ads inject कर रही है? या कुछ और गड़बड़?
CroxyProxy का client-side script काफ़ी ज़्यादा obfuscated है।
(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();
जिसे चलाने पर ये मिलता है:
function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...
इसे देखकर लगता है कि croxyproxy इस obfuscation के लिए Obfuscator.io इस्तेमाल कर रही है। जो कि ख़ुशक़िस्मती से webcrack से काफ़ी आसानी से deobfuscate हो जाता है।
उससे ये JavaScript काफ़ी readable बन जाता है:
((_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)) {
अच्छा है। अब कम से कम हम देख सकते हैं कि ये क्या कर रहा है। और... ज़्यादातर तो ठीक ही लगता है। मुझे लगता है obfuscation का मकसद ज़्यादातर ये है कि प्रॉक्सी को detect करना मुश्किल हो जाए। ज़्यादातर।
हमें कुछ 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;
}
और भी कुछ जगहें हैं, लेकिन कुल मिलाकर ये ads दिखा रहा है, जिनमें pop-under स्टाइल ads भी हैं। ये FuckAdBlock भी यूज़ करता है।
असल string replacement वगैरह तो server-side हो रहा है। और वहाँ क्या-क्या हो रहा होगा, कहा नहीं जा सकता।
जो भी हो, अगर तुम्हें अपने अकाउंट की security की ज़रा भी परवाह है तो तुम्हें वेब प्रॉक्सी का इस्तेमाल बिल्कुल नहीं करना चाहिए। अगर बहुत मजबूरी हो, तो कम से कम कोई भी PII / account / purchase information मत डालो।
"Resource swapping" को कूड़ेदान में फेंक देना
आख़िर में मैंने ये तय किया कि resource swapping वाली complexity, और foony.io सपोर्ट के लिए बाकियों जगहों पर जो extra complexity बढ़ रही थी, वो उन थोड़े से network savings के लायक नहीं है जो सुंदर, cookieless requests से मिल रहे थे। foony.io अपनाने के बाद से हम अपने gameplay conversions में एक अजीब गिरावट भी देख रहे थे, तो मुझे शक है कि foony.io के साथ और भी छुपी हुई दिक्कतें थीं जिनका हमें अंदाज़ा नहीं था।
तो मैंने foony.io हटा दिया। कम से कम अभी के लिए।
जैसे ही मैंने foony.io वाली CDN logic को हटा कर सब कुछ foony.com पर standard कर दिया, proxy सपोर्ट अचानक से बहुत ज़्यादा आसान हो गया:
- same-origin asset loads।
- प्रॉक्सी के ServiceWorker को समझाने के लिए कम “special cases”।
- कम rewriting।
- कम नाज़ुक कोड।
सीधे शब्दों में, foony.io हटाना एक architectural simplification थी, जिसने अजीब proxy behavior के लिए खुला छोड़ा हुआ surface area काफ़ी घटा दिया।
पास 3: क्या काम करता है, क्या नहीं, और क्यों
जो प्रॉक्सी पक्के तौर पर काम कर रही हैं
इस वक़्त तक Foony इन प्रॉक्सी के पीछे ठीक से चल रही है:
- croxyproxy
- proxyorb
कुछ और प्रॉक्सी भी शायद चल जाएँ। मुझे लगता है ज़्यादातर अब भी नहीं चलेंगी। लेकिन कम से कम वो अहम प्रॉक्सी, जिन्हें लोग games खेलने के लिए ज़्यादा यूज़ करते हैं, अब काम कर रही हैं।
“सारी प्रॉक्सी” क्यों नहीं?
कुछ प्रॉक्सी मॉडर्न multiplayer वेब ऐप को सपोर्ट करने लायक ही नहीं हैं। जैसे:
- प्रॉक्सी जो HTTPS को ठीक से सपोर्ट नहीं करतीं।
- प्रॉक्सी जो WebSockets को तोड़ देती हैं या block कर देती हैं (Foony real-time networking यूज़ करता है)। टेक्निकली इसे workaround किया जा सकता है, लेकिन complexity बहुत बढ़ जाएगी।
- प्रॉक्सी जिनमें cross-origin requests, headers या ServiceWorkers के लिए बहुत सख़्त पाबंदियाँ हैं।
मुख्य बातें
वेब प्रॉक्सी काफ़ी असुरक्षित होती हैं
ये ऐसा middleware हैं जो:
- HTML को rewrite करता है
- JavaScript को rewrite करता है
- कई बार अपना ServiceWorker inject कर देता है
- और अक्सर requests को route करने के लिए query params / URL encoding पर टिका होता है
- और तुम्हारी pages के साथ न जाने क्या-क्या कर सकता है
मुझे हैरानी हुई कि कुछ प्रॉक्सी कितना अंदर तक हस्तक्षेप करती हैं: ये shader source strings, comments, और पता नहीं क्या-क्या rewrite कर देती हैं।
कई बार सबसे अच्छा फ़िक्स architectural होता है
WebGL patch ने games को दोबारा render कर दिया, लेकिन multi-domain CDN strategy हटाने से proxy सपोर्ट लंबे समय के लिए stable हुआ।
ये याद दिलाता है कि कुछ चालाक optimizations पूरी तरह वाजिब लगते हैं, जब तक वो किसी hostile middleware से न टकरा जाएँ। या user के browser extensions से। या Safari से। या language settings से। या accessibility features से। या solar flares से। या किसी भी और चीज़ से, सच में।
निष्कर्ष
अब Foony उन प्रॉक्सी के पीछे काम करती है जो मायने रखती हैं (croxyproxy और proxyorb), और इसके लिए कोडबेस को proxy-specific गड़बड़ झोले में नहीं बदलना पड़ा:
- Three.js shaders के लिए एक generic फ़िक्स (कोई proxy-specific identifiers नहीं)।
- एक सरल domain strategy (हर जगह foony.com)।