background blurbackground mobile blur

1/1/1970

كيف جعلت Foony تعمل خلف البروكسيات

أهلًا! من زمان وأنا أعرف أن بروكسيات الويب تسبب مشاكل توافق للمواقع. مع ذلك، دعم Foony للعمل عبر البروكسي كان سيئًا بشكل مزعج، وحل مشكلة توافق Foony مع البروكسيات كان موضوعًا معقدًا فعلًا.

والمشكلة هنا ليست من نوع “Foony تستخدم واجهات برمجة غريبة” (مع إننا نفعل ذلك). كانت مزيجًا من:

  • بروكسيات تعيد كتابة النصوص بشكل عنيف في أماكن ما يفترض أصلًا يلمسوها.
  • بروكسيات تتعامل مع نطاق الموقع الرئيسي بشكل مختلف عن النطاقات "الأخرى" (CDN، مستضيفي الملفات، إلخ).
  • والحقيقة المزعجة أن بعض البروكسيات ببساطة لا تستطيع دعم تطبيقات الويب الحديثة (دعم HTTPS بشكل صحيح، WebSockets، وغيرهم).

Foony لا تعمل مع كل بروكسي موجود، لكنها تعمل الآن على الأقل مع croxyproxy و proxyorb، وهذا كان الهدف الأساسي.

فيما يلي أشرح ما الذي كان يتعطل، ولماذا، وما هي الحلول التي أحدثت فرقًا فعلًا.


المرحلة 1: شيدر Three.js صالح لكن معطوب

العرض

عندما جرّبت croxyproxy، لم أستطع لعب 8 Ball Pool ولا أيًّا من ألعاب Foony الأخرى المبنية على three.js مثل هذه. كنت أحصل باستمرار على فشل في ترجمة الشيدر في Three.js مع رسائل خطأ مثل:

  • “Shader Error 1282 - VALIDATE_STATUS false”

هذه الرسالة عديمة الفائدة تقريبًا؛ غالبًا معناها: "الشيدر عندك غير صالح، حظًا موفقًا". ممتاز. إذا تساءلت يومًا لماذا أستخدم رسائل خطأ فريدة لكل خطأ في Foony، فهذه هي الأسباب. الرسائل الواضحة تساعدك تحدد المشكلة بدل أن تقول لك فقط "الكود خربان، روح صلّحه".

لكن لماذا كانت شيدرات three.js الصالحة تمامًا تتعطل؟ ما القصة؟

السبب الحقيقي: البروكسيات تفسد layout(location = N)

Three.js تنتج كود GLSL يحتوي على محدِّدات layout مثل:

layout(location = 0) in vec3 position;

بعض البروكسيات تحاول إعادة كتابة أي شيء يشبه واجهة JavaScript location عن طريق استبدال النصوص بشكل ساذج على مستوى الملف كاملًا. هذا سيئ أصلًا في JavaScript، لكنهم كانوا يفعلون الشيء نفسه داخل نصوص الشيدر. أظن أن تحليل الـ AST مكلف عليهم زيادة عن اللزوم.

فصار نص الشيدر يتشوّه إلى شيء مثل:

layout(__cpLocation = 0) in vec3 position;

اسم المعرف هناك لابد أن يكون location. أي شيء آخر يعتبر GLSL غير صالح، وسيقوم المترجم برفضه. (محددات Layout في GLSL)

هذه مشكلة تخص Three.js فقط من ناحية أن Three.js تولّد الشيدرات ديناميكيًا، ونحن نمرّرها إلى WebGL وقت التشغيل. لكن العلة الحقيقية هي طريقة إعادة الكتابة التي يستعملها البروكسي.

لماذا لم أحاول "إصلاح البروكسي"

الطريقة الساذجة كانت ستكون أن أبحث عن سلسلة الاستبدال الخاصة بـ croxyproxy لكلمة location وهي __cpLocation ثم أستبدلها بـ location من جديد.

لكن البروكسيات المختلفة تستخدم أسماء استبدال مختلفة. بعضها يستخدم __cpLocation، وآخرون يستخدمون معرّفات غريبة أخرى. لذلك فإن كتابة حل ثابت من نوع "استبدل __cpLocation بـ location" حل هش جدًا.

كنت أحتاج إلى:

  • حل عام لا يعتمد على أسماء معيّنة لبروكسيات معينة.
  • حل يعمل حتى لو كان البروكسي يعيد كتابة كلمة location داخل JavaScript عندي أيضًا.

حيلة base64: إخفاء كلمة location عن البروكسي

إذا كان البروكسي يعيد كتابة كل ظهور حرفي لكلمة location يراها، أبسط خطوة هي ألّا أكتب location حرفيًا من الأساس. بسيط بما يكفي. شفت حيَلًا مشابهة لهذا من قبل في Lua عندما قمت بعكس نظام حماية الأدلة في RestedXP (إذا كنت أتذكر صح، هم يخفون طريقة استعمالهم للدالة BNGetInfo، مثلًا _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

هذه الحيلة تشتغل في JavaScript أيضًا طبعًا. في الملف client/index.html, أقوم بفك التشفير التالي وقت التشغيل:

// 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، لذلك لا يمكنه أن يفسد السلسلة مسبقًا. قسّمت السلسلة إلى جزئين لجعل اكتشافها أصعب، واستخدمت 'atob' فقط لأنه متاح، لكن استخدام String.fromCharCode أو تمثيل window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] بالهكس قد ينجح أيضًا.

نمط الشيدر المعطوب يكون دائمًا بنفس البنية تقريبًا:

layout(<something> = <number>)

لذلك أطابق هذا النمط بشكل عام وأستبدل <something> بالمعرّف الصحيح:

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

خطاف WebGL: ترقيع shaderSource (WebGL1 + WebGL2)

بما أن Three.js تستدعي gl.shaderSource(shader, source)، قمت بترقيع دالة 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,
});

وأطبّق الرقعة نفسها على WebGL2RenderingContext إذا كانت موجودة.

بعد أن أضفت هذا، اختفت أخطاء ترجمة الشيدر تمامًا. في هذه المرحلة، croxyproxy كان يعمل، لكن proxyorb ما زال يتعطل. لماذا؟ أليس من المفترض أن يعمل الاثنان بالطريقة نفسها؟


المرحلة 2: المشكلة الثانية (النطاقات) ولماذا جعل حذف foony.io كل شيء أسهل

تاريخيًا استخدمت Foony نطاقين، على الأقل خلال الشهر الماضي:

  • foony.com للموقع الرئيسي
  • foony.io للملفات الثابتة

السبب الأصلي كان عمليًا: تقديم الملفات من نطاق لا يحمل كوكيز يعني أنك تتجنب تضخّم ترويسة الكوكي في كل طلب لملف ثابت. هذا شيء ممتاز، لكنه ليس ضروريًا بالقدر الذي قد تتخيله، خصوصًا أن HTTP/2 يستعمل HPACK لتقليل حجم الترويسات المرسلة عبر الشبكة.

هذا تحسين منطقي في التصفح العادي.

لكن خلف البروكسيات، تحوّل إلى مصدر كبير للأعطال. وقاعدة مستخدمي Foony تعشق البروكسيات. تنهد

البروكسيات تتعامل مع "الموقع الرئيسي" بشكل مختلف عن "المواقع الأخرى"

الكثير من البروكسيات مُحسَّنة لسيناريو "اعبر هذا الموقع أو هذه الصفحة فقط". يقومون بتحميل صفحة الـ HTML الرئيسية بنجاح، وحقن سكربتاتهم، وتسجيل ServiceWorker خاص بهم، وغير ذلك.

لكن عندما يبدأ التطبيق في جلب الملفات من مصدر مختلف (مثل foony.io)، تدخل في عالم لطيف من الأعطال المسلية:

  • فشل في اعتراض الطلب من قبل ServiceWorker برسائل مثل:
    • “ServiceWorker intercepted the request and encountered an unexpected error”
    • “Loading failed for the module with source”
  • اعتماد بنية البروكسي الداخلية على باراميترات الاستعلام Query params (ومن السهل جدًا حذفها بالخطأ).
  • طلبات الملفات تفقد بيانات التوجيه الداخلية الخاصة بالبروكسي.
  • بروكسيات غريبة تستبدل الطلب كاملًا بعنوان مثل generic-php-slug.php?someQueryParam=hugeEncodedString (نعم، لم أحاول حتى دعم هذا النوع).

هذه الآليات الداخلية للبروكسي تعتمد على باراميترات الاستعلام / وترميز الروابط، وهي هشة جدًا.

أحد الأمثلة الواضحة كان رابط ملف يبدو هكذا:

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

?__pot=... هناك هو معلومة التوجيه / الحالة الخاصة بالبروكسي، والتي تخبره لأي نطاق ينتمي هذا الطلب. إذا أزلتها، لن يستطيع البروكسي معالجة الطلب بشكل صحيح، وستنتهي في مسار أخطاء الـ ServiceWorker.

"تبديل الموارد" للإنقاذ (ولماذا تعقّد كل شيء بسرعة)

في مرحلة ما، جرّبت حيلة التفافية: أكتشف أن "نحن خلف بروكسي"، ثم أبدّل أي رابط مورد يشير إلى foony.io ليستخدم المصدر الحالي نفسه، حتى يرى البروكسي كل شيء كأنه من نفس المصدر.

هذا يبدو معقولًا، وقد عمل بالفعل مع croxyproxy، لكنه أضاف قدرًا كبيرًا من التعقيد:

  • تحتاج إلى استبدال وسوم link وscript الموجودة أصلًا داخل الـ HTML.
  • تحتاج إلى MutationObserver للتعامل مع الوسوم التي تُحقن ديناميكيًا (مثل modulepreload، وملفات الأنماط، وغيره).
  • يجب أن تحافظ على باراميترات الاستعلام الخاصة بالبروكسي، وإلا كسرت نظام التوجيه لديه. والبروكسيات المختلفة تطبّق هذا بطرق مختلفة. طبعًا.
  • ومع ذلك ما زال عليك إبقاء المنطق عامًا (من دون متغيرات عالمية خاصة ببروكسي معيّن) حتى لا يتحول الكود إلى فوضى منتفخة لا تُحتمل.

وهنا عاد موضوع "حيلة base64" مرة أخرى: حتى في JavaScript الخاصة بي، كان علي أن أتوخى الحذر مع السلسلة الحرفية location لأن البروكسي قد يعيد كتابتها.

الهندسة العكسية للسكربت الذي يحقنه CroxyProxy

في هذه اللحظة زاد فضولي: ماذا يفعل البروكسي فعلًا بصفحتي؟ هل يحقن إعلاناته الخاصة؟ أم شيء أسوأ؟

السكربت الذي يستخدمه CroxyProxy على جهة العميل مشوَّش بشكل كبير.

(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 يستخدم Obfuscator.io لهذا التشويش. ولحسن الحظ يمكن فك هذا التشويش بسهولة كافية باستخدام webcrack.

والنتيجة هي 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)) {

جميل. الآن نستطيع أن نرى ما الذي يفعله. ويبدو... مقبولًا في الغالب. أظن أن التشويش موجود في الأساس حتى يصعُب اكتشاف أن الصفحة تعمل خلف بروكسي. في الغالب.

هناك بعض حقن الإعلانات / الواجهة:

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

هناك أجزاء أخرى، لكن بالمجمل هدفه عرض الإعلانات، بما فيها إعلانات من نوع pop-under. كما يستخدم مكتبة FuckAdBlock.

لكن الاستبدال الحقيقي للسلاسل النصية يحدث في الجهة الخلفية على الخادم. ولا أحد يعرف بالضبط كل ما يفعله هناك.

في كل الأحوال، إذا كانت تهمك سلامة حساباتك، فعليك ألا تستخدم بروكسيات الويب قدر الإمكان. وإذا اضطررت لاستخدامها، فتجنّب إدخال أي معلومات شخصية حساسة أو بيانات حساب أو تفاصيل شراء.

رمي "تبديل الموارد" في سلة المهملات

قررت أن التعقيد الناتج عن تبديل الموارد، مضافًا إليه التعقيد في أجزاء أخرى من الكود لدعم foony.io، لا يستحق مقدار التوفير البسيط في الشبكة من طلبات جميلة خالية من الكوكيز. كنا نرى أيضًا هبوطًا غير مبرَّر في عدد اللاعبين الذين يصلون فعلًا إلى مرحلة اللعب منذ أن بدأنا استخدام foony.io، لذا أظن أن هناك مشكلات أخرى في foony.io لم نكن ننتبه لها.

لذلك حذفت foony.io. على الأقل في الوقت الحالي.

ما إن أزلت منطق CDN الخاص بـ foony.io ووحّدت كل شيء على foony.com حتى أصبح دعم البروكسي أبسط بكثير:

  • تحميل الملفات من نفس المصدر (same-origin).
  • حالات خاصة أقل تحتاج لشرح لـ ServiceWorker الخاص بالبروكسي.
  • عمليات إعادة كتابة أقل.
  • كود أقل هشاشة.

باختصار، إزالة foony.io كانت تبسيطًا معماريًا قلّل المساحة المتاحة للتصرفات الغريبة من طرف البروكسيات.


المرحلة 3: ما الذي يعمل، وما الذي لا يعمل، ولماذا

البروكسيات المؤكَّد عملها

في هذه المرحلة، Foony تعمل خلف:

  • croxyproxy
  • proxyorb

على الأرجح هناك بروكسيات أخرى تعمل أيضًا. وأغلب الظن أن معظمها ما زال لا يعمل. لكن على الأقل البروكسيات المهمة التي يستخدمها الناس للّعب تبدو أنها تعمل الآن.

لماذا لا تعمل مع "كل" البروكسيات؟

بعض البروكسيات ببساطة لا تستطيع دعم تطبيق ويب حديث متعدد اللاعبين. مثلًا:

  • بروكسيات لا تدعم HTTPS بشكل صحيح.
  • بروكسيات تكسر أو تحجب WebSockets (Foony تستخدم شبكة لحظية). من الناحية النظرية يمكن الالتفاف على هذا، لكنه سيضيف تعقيدًا كبيرًا.
  • بروكسيات تفرض قيودًا كبيرة على الطلبات عبر المصادر المختلفة، أو على الترويسات، أو على ServiceWorkers.

أهم ما يمكن استخلاصه

بروكسيات الويب غير آمنة جدًا

هي طبقة وسطية تقوم بـ:

  • إعادة كتابة الـ HTML
  • إعادة كتابة JavaScript
  • أحيانًا حقن ServiceWorker خاص بها
  • وغالبًا تعتمد على باراميترات الاستعلام / وترميز الروابط لتوجيه الطلبات
  • ويمكنها العبث بصفحاتك بطرق لا حصر لها

اندهشت من مدى العمق الذي تصل إليه بعض البروكسيات؛ تعيد كتابة نصوص الشيدر نفسها, والتعليقات, والله أعلم ماذا أيضًا.

أحيانًا يكون أفضل حل هو تغيير البنية نفسها

رقعة WebGL جعلت الألعاب تُعرض من جديد، لكن التخلّي عن استراتيجية الـ CDN متعددة النطاقات هو ما جعل دعم البروكسي يبقى مستقرًا.

وهذا تذكير جيد لنا جميعًا: التحسينات الذكية قد تكون منطقية تمامًا حتى تصطدم بطبقة وسيطة عدائية. أو بإضافة متصفح عند المستخدم. أو Safari. أو إعدادات اللغة. أو ميزات الوصول. أو نشاط شمسي عشوائي. أو أي شيء آخر حرفيًا.


الخاتمة

Foony تعمل الآن خلف البروكسيات المهمة (croxyproxy و proxyorb)، من دون أن يتحول الكود إلى فوضى مليئة بحلول خاصة بكل بروكسي على حدة:

  • إصلاح عام لشيدرات Three.js (من دون معرّفات خاصة ببروكسي معيّن).
  • استراتيجية أبسط للنطاقات (اعتماد foony.com في كل مكان).
8 Ball Pool online multiplayer billiards icon