background blurbackground mobile blur

1/1/1970

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

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

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

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

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

في الأسفل أشرح ما الذي تعطّل، ولماذا تعطّل، والإصلاحات التي أحدثت فرقًا حقيقيًا.


المحاولة الأولى: شيدرات 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(location = 0) in vec3 position;

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

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

layout(__cpLocation = 0) in vec3 position;

المعرّف يجب أن يكون location هناك. أي شيء آخر هو GLSL غير صالح، والمُجمِّع يرفضه. (معرّفات التخطيط في GLSL)

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

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

النهج الساذج هو البحث عن سلسلة الاستبدال الخاصة بـ croxyproxy، أي __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، أقوم بفك ترميز ما يلي وقت التشغيل:

// لأن هذه البروكسيات تحاول استبدال كل `location`، نستخدم سلسلة مُرمَّزة بـ base64.
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(<شيء ما> = <رقم>)

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

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 كان لا يزال يفشل. لماذا؟! ألا يجب أن يعمل بنفس الطريقة؟


المحاولة الثانية: المشكلة الثانية (النطاقات) ولماذا جعل إزالة 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 (نعم، لم أتكبد عناء دعم ذلك).

تعتمد الآليات الداخلية لهذه البروكسيات على معاملات الاستعلام / ترميز عناوين URL، وهي هشّة للغاية.

أحد الأمثلة الواضحة كان عنوان URL لأصل مثل:

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

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

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

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

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

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

هنا أيضًا ظهرت "حيلة 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، أصبح دعم البروكسي أبسط بشكل كبير:

  • تحميل الأصول من نفس المصدر.
  • حالات "خاصة" أقل لشرحها لـ ServiceWorker الخاص بالبروكسي.
  • إعادة كتابة أقل.
  • كود أقل هشاشة.

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


المحاولة الثالثة: ما يعمل، وما لا يعمل، ولماذا

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

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

  • croxyproxy
  • proxyorb

ربما تعمل بعض البروكسيات الأخرى. أراهن أن معظمها لا يزال لا يعمل. لكن على الأقل البروكسيات المهمة التي يستخدمها الناس للعب الألعاب تبدو أنها تعمل.

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

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

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

النقاط الرئيسية

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

إنها برامج وسيطة:

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

فوجئتُ كم تذهب بعض البروكسيات في العمق: ستُعيد كتابة سلاسل مصدر الشيدر، التعليقات، والله أعلم بماذا أيضًا.

أحيانًا يكون أفضل إصلاح هو إصلاح معماري

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

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


الخاتمة

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

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