background blurbackground mobile blur

1/1/1970

Cách Mình Làm Cho Foony Hoạt Động Sau Proxy

Chào bạn! Mình đã biết từ lâu rằng các web proxy thường gây ra vấn đề tương thích cho các website. Tuy nhiên, khả năng hỗ trợ của Foony trên proxy thì tệ nổi tiếng, và việc giải quyết vấn đề tương thích proxy của Foony khá là hóc búa.

Đây cũng không phải vấn đề kiểu "Foony dùng API kỳ lạ" đâu (mặc dù bọn mình có dùng thật). Nó là sự kết hợp của:

  • Proxy thực hiện việc viết lại chuỗi một cách quá đà ở những nơi mà tuyệt đối không nên đụng vào.
  • Proxy đối xử với domain chính của site khác với các domain "khác" (CDN, host tài nguyên, v.v.).
  • Và sự thật phũ phàng là một số proxy đơn giản không thể hỗ trợ các ứng dụng web hiện đại (HTTPS đúng chuẩn, WebSocket, v.v.).

Bọn mình không hoạt động được với mọi proxy, nhưng giờ thì ít nhất đã chạy được với croxyproxyproxyorb, đó là mục tiêu.

Dưới đây mình giải thích cái gì hỏng, vì sao hỏng, và những bản sửa lỗi thực sự quan trọng.


Lượt 1: Shader Three.js hợp lệ nhưng bị hỏng

Triệu chứng

Khi mình thử croxyproxy, mình không thể chơi 8 Ball Pool hay bất kỳ game three.js nào khác của Foony. Mình liên tục gặp lỗi biên dịch shader trong Three.js với thông báo kiểu:

  • "Shader Error 1282 - VALIDATE_STATUS false"

Thông báo đó gần như vô dụng hoàn toàn. Nó thường có nghĩa là "shader của bạn không hợp lệ, chúc may mắn." Tuyệt vời. Nếu bạn từng thắc mắc tại sao mình luôn dùng thông báo lỗi độc nhất cho từng lỗi trên Foony, thì đây chính là lý do. Nó giúp xác định chính xác vấn đề thay vì kiểu "code hỏng, tự đi mà sửa."

Nhưng tại sao những shader three.js hoàn toàn hợp lệ lại bị hỏng? Có chuyện gì vậy?

Nguyên nhân thực sự: proxy làm hỏng layout(location = N)

Three.js sinh ra GLSL với các layout qualifier kiểu:

layout(location = 0) in vec3 position;

Một số proxy cố gắng viết lại bất cứ thứ gì trông giống API JavaScript location bằng cách thay thế chuỗi toàn cục một cách ngây thơ. Điều đó đã đủ tệ trong JS rồi, nhưng họ còn làm điều đó cả bên trong chuỗi mã nguồn shader. Chắc là parse AST tốn quá đối với họ.

Vậy nên mã nguồn shader bị làm hỏng thành kiểu:

layout(__cpLocation = 0) in vec3 position;

Định danh ở đó bắt buộc phải là location. Bất cứ thứ gì khác đều là GLSL không hợp lệ, và trình biên dịch sẽ từ chối. (Layout Qualifiers trong GLSL)

Đây chỉ là vấn đề của Three.js theo nghĩa là Three.js sinh shader một cách động, và bọn mình truyền chúng vào WebGL lúc runtime. Lỗi thực sự nằm ở chiến lược viết lại của proxy.

Tại sao mình không "sửa proxy"

Cách tiếp cận ngây thơ là tìm chuỗi thay thế location của croxyproxy là __cpLocation, rồi đổi lại thành location. Tuy nhiên, các proxy khác nhau dùng tên thay thế khác nhau. Một số dùng __cpLocation, số khác dùng những định danh kỳ lạ khác. Vậy nên việc hardcode kiểu "đổi __cpLocation về location" rất mong manh.

Mình cần:

  • Một bản sửa tổng quát (không hardcode định danh proxy nào).
  • Một bản sửa hoạt động kể cả khi proxy đang viết lại từ location trong JavaScript của mình nữa.

Mẹo base64: giấu từ location khỏi proxy

Nếu proxy viết lại mọi từ location mà nó thấy, cách đơn giản nhất là... đừng dùng location. Dễ thôi. Mình từng thấy mẹo này trước đây trong Lua khi reverse-engineer hệ thống bảo vệ guide của RestedXP (nếu mình nhớ không nhầm, họ làm rối việc sử dụng BNGetInfo, ví dụ _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Mẹo này tất nhiên cũng hoạt động trong JavaScript. Trong client/index.html, mình giải mã đoạn sau lúc runtime:

// Vì các proxy này cố thay thế mọi `location`, ta dùng chuỗi mã hóa base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

Cái atob() đó xảy ra sau khi proxy đã thực hiện viết lại HTML/JS rồi, nên nó không thể "làm hỏng trước" chuỗi đó. Mình chia chuỗi thành hai phần để khó phát hiện hơn, và mình dùng 'atob' vì có thể, nhưng String.fromCharCode hoặc hex-escape kiểu window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] cũng có thể hoạt động.

Mẫu shader bị hỏng luôn có cấu trúc giống nhau:

layout(<gì đó> = <số>)

Vậy nên mình match nó tổng quát rồi thay <gì đó> bằng định danh đúng:

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

Hook WebGL: vá shaderSource (WebGL1 + WebGL2)

Vì Three.js gọi gl.shaderSource(shader, source), mình vá luôn 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,
});

Và mình áp dụng cùng bản vá cho WebGL2RenderingContext nếu nó tồn tại.

Khi đoạn đó được đặt vào, các lỗi biên dịch shader đã biến mất. Đến lúc này, croxyproxy đã chạy nhưng proxyorb vẫn fail. Sao thế?! Lẽ ra phải hoạt động giống nhau chứ?


Lượt 2: Vấn đề thứ hai (domain) và tại sao việc bỏ foony.io làm mọi thứ dễ hơn

Foony trong lịch sử dùng hai domain, ít nhất là trong tháng vừa qua:

  • foony.com cho site chính
  • foony.io cho tài nguyên tĩnh

Lý do ban đầu mang tính thực tế: phục vụ tài nguyên từ một domain không có cookie tránh được việc phình header cookie ở mỗi request file tĩnh. Điều này tuyệt đấy, nhưng không cần thiết lắm như bạn nghĩ vì HTTP/2 dùng HPACK để giảm số byte gửi qua mạng cho header.

Đó là một tối ưu hợp lý trong duyệt web bình thường.

Nhưng sau proxy, nó trở thành nguồn gây hỏng chính. Và lượng người dùng Foony lại rất thích proxy. thở dài

Proxy đối xử với "site chính" khác với "các site khác"

Nhiều proxy được tối ưu cho "proxy trang/domain duy nhất này." Chúng load HTML chính thành công, inject script, đăng ký ServiceWorker riêng, v.v.

Nhưng khi ứng dụng bắt đầu kéo tài nguyên từ một origin khác (như foony.io), bạn sẽ gặp đủ kiểu vui vẻ hỏng hóc:

  • Lỗi chặn của ServiceWorker kiểu:
    • "ServiceWorker intercepted the request and encountered an unexpected error"
    • "Loading failed for the module with source"
  • Tham số query bắt buộc phải có cho hạ tầng proxy (và rất dễ vô tình bị cắt mất).
  • Request tài nguyên mất đi metadata định tuyến nội bộ của proxy.
  • Proxy kỳ lạ thay thế toàn bộ request bằng generic-php-slug.php?someQueryParam=hugeEncodedString (ờ, mình không buồn hỗ trợ cái đó).

Cơ chế nội bộ của các proxy này phụ thuộc vào tham số query / mã hóa URL, và chúng khá là mong manh.

Một ví dụ điển hình là URL tài nguyên kiểu:

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

Cái ?__pot=... đó là routing/state riêng của proxy báo cho proxy biết request này dành cho domain nào. Nếu cắt nó đi, proxy không thể giải quyết request đúng cách, và bạn rơi vào đường đi lỗi của ServiceWorker.

"Hoán đổi tài nguyên" để giải cứu (và tại sao nó nhanh chóng phức tạp)

Có lúc mình đã thử workaround: phát hiện "ta đang bị proxy," rồi đổi mọi URL tài nguyên foony.io thành origin hiện tại để proxy thấy mọi thứ là same-origin.

Nghe hợp lý đấy, và nó hoạt động với croxyproxy, nhưng nó thêm rất nhiều phức tạp:

  • Bạn cần thay thế các thẻ linkscript đã có sẵn trong HTML.
  • Bạn cần MutationObserver để xử lý các thẻ được inject động (modulepreload, stylesheet, v.v.).
  • Bạn phải bảo toàn tham số query của proxy, không thì làm hỏng routing của họ. Và proxy khác nhau làm chuyện này khác nhau. Tất nhiên rồi.
  • Và bạn vẫn phải giữ logic tổng quát (không có biến toàn cục riêng cho proxy nào) để code không trở thành một mớ hỗn loạn.

Đây cũng là chỗ "mẹo base64" lại xuất hiện: ngay cả trong JavaScript của chính mình, mình phải cẩn thận với chuỗi location vì proxy có thể viết lại nó.

Reverse-engineer script được inject của CroxyProxy

Đến đây mình tò mò: proxy này thực sự làm gì với trang của mình? Có inject quảng cáo riêng không? Hay tệ hơn?

Script phía client của CroxyProxy bị làm rối nặng.

(new Function(new TextDecoder('utf-8').decode(new Uint8Array((atob('NjY3NTZlN...')).match(/.{1,2}/g).map(b => parseInt(b, 16))))))();

Khi chạy, kết quả là:

function a0_0x5ebf(_0x213dc9,_0x1c49b6){var _0x4aa7c1=a0_0x4274();return a0_0x5ebf=function(_0x159600,_0x51d898){_0x159600=...

Dựa vào đó, có vẻ croxyproxy đang dùng Obfuscator.io để làm rối. May là cái này khá dễ giải rối với webcrack.

Kết quả là JavaScript dễ đọc hơn nhiều:

((_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)) {

Tuyệt. Giờ ta có thể thấy nó đang làm gì. Và... có vẻ phần lớn là ổn. Mình nghĩ việc làm rối chủ yếu để giúp tránh bị phát hiện proxy. Chủ yếu thôi.

Có một số inject quảng cáo / 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;
    }

Còn vài chỗ khác, nhưng cơ bản chỉ là hiển thị quảng cáo, bao gồm cả quảng cáo dạng pop-under. Nó cũng dùng FuckAdBlock.

Tuy nhiên, việc thay thế chuỗi thực sự diễn ra ở phía server. Và trời mới biết nó đang làm gì.

Dù sao đi nữa, bạn tuyệt đối không nên dùng web proxy nếu bạn quan tâm đến bảo mật tài khoản. Nếu phải dùng, tránh nhập bất kỳ thông tin cá nhân / tài khoản / mua hàng nào.

"Hoán đổi tài nguyên" vào sọt rác

Mình quyết định rằng sự phức tạp từ việc hoán đổi tài nguyên, kết hợp với sự phức tạp ở các phần khác trong code để hỗ trợ foony.io, không đáng với khoản tiết kiệm mạng nhỏ nhoi từ những request đẹp đẽ không cookie. Bọn mình cũng thấy tỷ lệ chuyển đổi gameplay giảm không rõ lý do từ khi áp dụng foony.io, nên mình nghi ngờ có những vấn đề khác với foony.io mà bọn mình không biết.

Vậy nên mình bỏ foony.io. Ít nhất là tạm thời.

Sau khi mình xóa logic CDN foony.io và chuẩn hóa mọi thứ về foony.com, hỗ trợ proxy trở nên đơn giản hơn rất nhiều:

  • Tài nguyên load same-origin.
  • Ít "trường hợp đặc biệt" hơn để giải thích cho ServiceWorker của proxy.
  • Ít việc viết lại hơn.
  • Code ít mong manh hơn.

Tóm lại, việc bỏ foony.io là một sự đơn giản hóa kiến trúc làm giảm bề mặt tiếp xúc với hành vi kỳ quặc của proxy.


Lượt 3: Cái gì hoạt động, cái gì không, và tại sao

Các proxy đã xác nhận hoạt động

Đến giờ, Foony chạy được sau:

  • croxyproxy
  • proxyorb

Một số proxy khác có thể hoạt động. Mình cá là phần lớn vẫn không. Nhưng ít nhất những cái quan trọng mà người ta dùng để chơi game thì có vẻ chạy được.

Tại sao không phải "tất cả proxy"?

Một số proxy đơn giản không thể hỗ trợ một ứng dụng web multiplayer hiện đại. Ví dụ:

  • Proxy không hỗ trợ HTTPS đúng cách.
  • Proxy làm hỏng hoặc chặn WebSocket (Foony dùng mạng thời gian thực). Về mặt kỹ thuật bạn có thể né được, nhưng sẽ thêm phức tạp.
  • Proxy có quá nhiều hạn chế xung quanh request cross-origin, header, hoặc ServiceWorker.

Bài học rút ra

Web proxy rất không an toàn

Chúng là middleware:

  • viết lại HTML
  • viết lại JavaScript
  • đôi khi inject một ServiceWorker
  • và thường phụ thuộc vào tham số query / mã hóa URL để định tuyến request
  • có thể nghịch ngợm với trang của bạn theo vô số cách

Mình ngạc nhiên là một số proxy đi sâu tới mức nào: chúng viết lại cả chuỗi mã nguồn shader, comment, và Chúa biết còn gì nữa.

Đôi khi bản sửa tốt nhất là về kiến trúc

Bản vá WebGL làm các game render lại được, nhưng việc bỏ chiến lược CDN nhiều domain mới làm hỗ trợ proxy duy trì được sự ổn định.

Đây là lời nhắc nhở tốt: những tối ưu thông minh có thể hoàn toàn hợp lý cho đến khi chúng va chạm với middleware thù địch. Hoặc tiện ích trình duyệt của user. Hoặc Safari. Hoặc cài đặt ngôn ngữ. Hoặc tính năng trợ năng. Hoặc bão mặt trời. Hoặc bất cứ thứ gì, thật sự.


Kết luận

Foony giờ đã chạy được sau những proxy quan trọng (croxyproxy và proxyorb), mà không biến codebase thành một mớ hỗn loạn riêng cho proxy:

  • Một bản sửa shader Three.js tổng quát (không có định danh riêng cho proxy).
  • Một chiến lược domain đơn giản hơn (foony.com ở mọi nơi).
8 Ball Pool online multiplayer billiards icon