background blurbackground mobile blur

1/1/1970

Як я змусив Foony працювати за проксі

Привіт! Я давно знаю, що веб-проксі спричиняють проблеми сумісності для вебсайтів. Однак підтримка Foony в проксі була особливо поганою, і вирішити питання сумісності Foony з проксі виявилося доволі складним.

Це не той випадок, коли «Foony використовує екзотичні API» (хоча ми й справді так робимо). Це була комбінація факторів:

  • Проксі агресивно переписують рядки в місцях, де вони абсолютно не повинні цього робити.
  • Проксі ставляться до основного домену сайту інакше, ніж до «інших» доменів (CDN, хости активів тощо).
  • І сувора реальність: деякі проксі просто не можуть підтримувати сучасні вебзастосунки (коректність HTTPS, WebSockets тощо).

Ми працюємо не з кожним проксі, але тепер ми працюємо принаймні з 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 API location, виконуючи наївну глобальну заміну рядків. Це вже погано в JS, але вони робили це й усередині рядків коду шейдерів. Гадаю, парсинг AST для них надто дорогий.

Отже, код шейдера був зіпсований до чогось такого:

layout(__cpLocation = 0) in vec3 position;

Ідентифікатор там повинен бути location. Будь-що інше є недійсним GLSL, і компілятор його відхиляє. (Кваліфікатори Layout у GLSL)

Це проблема Three.js лише в тому сенсі, що Three.js генерує шейдери динамічно, і ми передаємо їх до WebGL під час виконання. Справжній баг полягає в стратегії переписування проксі.

Чому я не «виправив проксі»

Наївний підхід полягав би в пошуку рядка-замінника location від 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 я декодую наступне під час виконання:

// 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 або hex-екранування 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 для статичних активів

Початкова причина була практичною: подавання активів з домену без cookies дозволяє уникнути роздування заголовків cookies при кожному запиті статичного файлу. Це чудово, але не настільки необхідно, як можна було б подумати, враховуючи, що 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»
  • Параметри запиту, які потрібні інфраструктурі проксі (і які легко випадково обрізати).
  • Запити активів втрачають внутрішні метадані маршрутизації проксі.
  • Дивні проксі, які замінюють весь запит на generic-php-slug.php?someQueryParam=hugeEncodedString (так, я не став морочитися з підтримкою цього).

Внутрішні механізми цих проксі залежать від параметрів запиту / URL-кодування, і вони доволі крихкі.

Один із показових прикладів — URL активу, як-от:

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

Цей ?__pot=... — це власна маршрутизація / стан проксі, який повідомляє проксі, для якого домену призначено запит. Якщо його прибрати, проксі не зможуть правильно вирішити запит, і ви опинитеся в шляху помилки ServiceWorker.

«Підміна ресурсів» на допомогу (і чому це швидко ускладнилося)

У якийсь момент я спробував обхідне рішення: виявити «нас проксіюють», а потім підмінити будь-які URL-адреси ресурсів foony.io на поточне джерело, щоб проксі бачив усе як same-origin.

Це звучить розумно, і це працювало для croxyproxy, але це додало багато складності:

  • Потрібно замінити теги link і script, які вже існують у HTML.
  • Потрібен MutationObserver для обробки динамічно інжектованих тегів (modulepreload, stylesheet тощо).
  • Потрібно зберегти параметри запиту проксі, інакше ви зламаєте їхню маршрутизацію. І різні проксі роблять це по-різному. Бо ж, звичайно.
  • І все одно потрібно тримати логіку загальною (без специфічних для проксі глобальних змінних), щоб код не перетворився на роздуте звалище.

Це також місце, де знову виник «трюк із 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)) {

Чудово. Тепер ми бачимо, що він робить. І... здається, здебільшого все нормально. Я думаю, обфускація переважно потрібна для запобігання виявленню проксі. Переважно.

Маємо трохи інжекції реклами / 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;
    }

Є ще кілька місць, але в основному він просто показує рекламу, включно з рекламою у стилі pop-under. Він також використовує FuckAdBlock.

Справжня заміна рядків, однак, відбувається на стороні сервера. І хто знає, що там усе робиться.

У будь-якому разі, вам абсолютно не варто використовувати веб-проксі, якщо вас турбує безпека вашого облікового запису. Якщо вже мусите, уникайте введення будь-якої вашої особистої / облікової / платіжної інформації.

«Підміна ресурсів» у смітник

Я вирішив, що складність від підміни ресурсів, у поєднанні зі складністю в інших частинах коду для підтримки foony.io, не варта невеликої економії мережі від красивих, безкукових запитів. Ми також спостерігали незрозуміле падіння конверсій ігор з моменту впровадження foony.io, тож я підозрюю, що з foony.io були й інші проблеми, про які ми не знали.

Тож я видалив foony.io. Принаймні поки що.

Як тільки я видалив CDN-логіку foony.io і стандартизував усе на foony.com, підтримка проксі стала драматично простішою:

  • Завантаження активів з того самого джерела.
  • Менше «особливих випадків», які потрібно пояснювати ServiceWorker проксі.
  • Менше переписування.
  • Менш крихкий код.

Коротше кажучи, видалення foony.io було архітектурним спрощенням, яке зменшило поверхню для дивної поведінки проксі.


Прохід 3: Що працює, що ні, і чому

Підтверджені робочі проксі

На цьому етапі 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