background blurbackground mobile blur

1/1/1970

Comment j'ai fait fonctionner Foony derrière les proxies

Salut ! Je sais depuis longtemps que les proxies web causent des problèmes de compatibilité avec les sites. Cependant, le support de Foony dans les proxies était notoirement mauvais, et résoudre la compatibilité de Foony avec les proxies a été plutôt délicat.

Et ce n'est même pas un problème du genre « Foony utilise des API exotiques » (même si c'est le cas). C'était une combinaison de :

  • Proxies qui font de la réécriture de chaînes agressive là où ils ne devraient absolument pas.
  • Proxies qui traitent le domaine principal du site différemment des « autres » domaines (CDN, hébergeurs d'assets, etc.).
  • Et la dure réalité que certains proxies ne peuvent tout simplement pas supporter les applis web modernes (correction HTTPS, WebSockets, etc.).

On ne fonctionne pas avec tous les proxies, mais on fonctionne maintenant au moins avec croxyproxy et proxyorb, ce qui était l'objectif.

Ci-dessous j'explique ce qui cassait, pourquoi ça cassait, et les correctifs qui ont vraiment compté.


Étape 1 : des shaders Three.js valides mais cassés

Le symptôme

Quand j'ai essayé croxyproxy, je n'arrivais pas à jouer au 8 Ball Pool ni à aucun autre jeu three.js de Foony. Je recevais sans cesse une erreur de compilation de shader dans Three.js avec des messages comme :

  • « Shader Error 1282 - VALIDATE_STATUS false »

Ce message était presque entièrement inutile. Ça veut généralement dire « votre shader est invalide, bonne chance ». Super. Si vous vous demandez pourquoi j'utilise toujours des messages d'erreur uniques pour chaque erreur sur Foony, c'est exactement pour ça. Ça aide à cibler les problèmes au lieu de simplement « le code est cassé, va le réparer ».

Mais pourquoi des shaders three.js parfaitement valides cassaient-ils ? Qu'est-ce qui se passe ?

La vraie cause : les proxies corrompent layout(location = N)

Three.js émet du GLSL avec des qualificateurs de layout comme :

layout(location = 0) in vec3 position;

Certains proxies essaient de réécrire tout ce qui ressemble à l'API JavaScript location en faisant un remplacement global naïf de chaînes. C'est déjà problématique en JS, mais ils le faisaient aussi à l'intérieur des chaînes sources de shaders. J'imagine que le parsing AST est trop coûteux pour eux.

Du coup la source du shader se retrouvait corrompue en quelque chose comme :

layout(__cpLocation = 0) in vec3 position;

L'identifiant doit être location ici. Toute autre chose est du GLSL invalide, et le compilateur le rejette. (Layout Qualifiers in GLSL)

C'est un problème Three.js seulement dans le sens où Three.js génère des shaders dynamiquement, et où on les passe à WebGL au runtime. Le vrai bug, c'est la stratégie de réécriture du proxy.

Pourquoi je n'ai pas « corrigé le proxy »

Une approche naïve serait de chercher la chaîne de remplacement de location de croxyproxy, __cpLocation, et de la remplacer par location. Cependant, différents proxies utilisent différents noms de remplacement. Certains utilisent __cpLocation, d'autres utilisent d'autres identifiants bizarres. Donc coder en dur un correctif comme « remplacer __cpLocation par location » est fragile.

Il me fallait :

  • Un correctif générique (pas d'identifiants de proxy codés en dur).
  • Un correctif qui marche même si le proxy réécrit aussi le mot location dans mon JavaScript.

L'astuce base64 : cacher le mot location au proxy

Si le proxy réécrit chaque location littéral qu'il voit, le plus simple est de tout simplement ne pas utiliser location. Facile. J'ai déjà vu cette astuce avant en Lua quand j'ai fait du reverse engineering sur le système de protection des guides de RestedXP (si je me souviens bien, ils obfusquent leur usage de BNGetInfo, par ex. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Cette astuce marche en JavaScript aussi, bien sûr. Dans client/index.html, je décode ce qui suit au runtime :

// Comme ces proxies essaient de remplacer chaque `location`, on utilise une chaîne encodée en base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location

Cet atob() se fait après que le proxy ait déjà fait sa réécriture HTML/JS, donc il ne peut pas « pré-corrompre » la chaîne. Je sépare la chaîne en deux pour rendre la détection encore plus difficile, et j'utilise 'atob' parce que je peux, mais String.fromCharCode ou l'échappement hex window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] pourraient aussi marcher.

Le motif des shaders cassés est toujours structurellement le même :

layout(<quelque_chose> = <nombre>)

Donc je le détecte de manière générique et je remplace <quelque_chose> par l'identifiant correct :

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

Le hook WebGL : patcher shaderSource (WebGL1 + WebGL2)

Comme Three.js appelle gl.shaderSource(shader, source), je patche shaderSource lui-même :

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

Et j'applique le même patch à WebGL2RenderingContext s'il existe.

Une fois ça en place, les erreurs de compilation des shaders ont disparu. À ce stade, croxyproxy fonctionnait, mais proxyorb échouait toujours. Pourquoi ?! Ça devrait fonctionner pareil, non ?


Étape 2 : le second problème (les domaines) et pourquoi supprimer foony.io a tout simplifié

Foony utilisait historiquement deux domaines, au moins ces derniers mois :

  • foony.com pour le site principal
  • foony.io pour les assets statiques

La raison initiale était pratique : servir les assets depuis un domaine sans cookies évite que les en-têtes de cookies ne gonflent l'upload de chaque requête de fichier statique. C'est génial, mais pas aussi nécessaire qu'on pourrait le penser, vu que HTTP/2 utilise HPACK pour réduire les octets envoyés sur le réseau pour les en-têtes.

C'est une optimisation valide en navigation normale.

Derrière les proxies, c'est devenu une source majeure de pannes. Et la base d'utilisateurs de Foony adore les proxies. soupir

Les proxies traitent « le site principal » différemment des « autres sites »

Beaucoup de proxies sont optimisés pour « proxifier cette page / ce domaine ». Ils chargeront le HTML principal avec succès, injecteront des scripts, enregistreront leur propre ServiceWorker, etc.

Mais quand l'appli commence à tirer des assets depuis une origine différente (comme foony.io), on se retrouve avec toutes sortes de petites joyeusetés :

  • Échecs d'interception du ServiceWorker comme :
    • « ServiceWorker intercepted the request and encountered an unexpected error »
    • « Loading failed for the module with source »
  • Paramètres de requête requis par l'infrastructure du proxy (et faciles à supprimer accidentellement).
  • Requêtes d'assets perdant les métadonnées de routage interne du proxy.
  • Proxies bizarres qui remplacent toute la requête par generic-php-slug.php?someQueryParam=hugeEncodedString (ouais, je n'ai pas pris la peine de supporter ça).

Les mécanismes internes de ces proxies dépendent des paramètres de requête / encodage d'URL, et ils sont assez fragiles.

Un des exemples révélateurs était une URL d'asset comme :

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

Ce ?__pot=... est le routage/état propre au proxy qui lui dit pour quel domaine la requête est destinée. Si tu le supprimes, les proxies ne peuvent pas résoudre la requête correctement, et tu finis avec le chemin d'erreur du ServiceWorker.

Le « resource swapping » à la rescousse (et pourquoi c'est devenu compliqué très vite)

À un moment, j'ai essayé un contournement : détecter « on est proxifié », puis échanger toutes les URL de ressources foony.io pour l'origine actuelle afin que le proxy voie tout comme étant de la même origine.

Ça paraît raisonnable, et ça fonctionnait pour croxyproxy, mais ça ajoutait beaucoup de complexité :

  • Il faut remplacer les balises link et script qui existent déjà dans le HTML.
  • Il faut un MutationObserver pour gérer les balises injectées dynamiquement (modulepreload, stylesheet, etc.).
  • Il faut préserver les paramètres de requête du proxy, sinon tu casses son routage. Et différents proxies font ça différemment. Évidemment.
  • Et il faut quand même garder la logique générique (pas de globales spécifiques au proxy) pour que le code ne devienne pas un capharnaüm boursouflé.

C'est aussi là que l'« astuce base64 » est revenue : même dans mon propre JavaScript, je devais faire attention à la chaîne littérale location parce que le proxy pourrait la réécrire.

Reverse engineering du script injecté par CroxyProxy

À ce moment-là, ça m'a intrigué : qu'est-ce que le proxy fait vraiment à ma page ? Est-ce qu'il injecte ses propres pubs ? Quelque chose de pire ?

Le script côté client de CroxyProxy est lourdement obfusqué.

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

Qui une fois exécuté, donne :

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

Sur la base de ça, on dirait que croxyproxy utilise Obfuscator.io pour cette obfuscation. Ce qui est heureusement assez facile à désobfusquer avec webcrack.

Ça donne du JavaScript bien plus lisible :

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

Pas mal. Maintenant on peut voir ce qu'il fait. Et... ça semble globalement correct. Je pense que l'obfuscation sert surtout à empêcher la détection du proxy. Surtout.

On a un peu d'injection de pub / d'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;
    }

Il y a quelques autres endroits, mais en gros ça affiche juste des pubs, y compris des pubs en pop-under. Ça utilise aussi FuckAdBlock.

Le vrai remplacement des chaînes, par contre, se fait côté serveur. Et qui sait tout ce que ça fait.

Quoi qu'il en soit, vous ne devriez absolument pas utiliser de proxies web si vous tenez à la sécurité de votre compte. Si vous devez le faire, évitez d'entrer toute info personnelle / de compte / d'achat.

« Resource swapping » à la poubelle

J'ai décidé que la complexité du resource swapping, couplée à la complexité dans d'autres parties du code pour le support de foony.io, ne valait pas les petites économies réseau de belles requêtes sans cookies. On voyait aussi une baisse inexpliquée de nos conversions de gameplay depuis l'adoption de foony.io, donc je soupçonne qu'il y avait d'autres soucis avec foony.io dont on n'avait pas conscience.

Alors j'ai supprimé foony.io. Au moins pour l'instant.

Une fois que j'ai supprimé la logique CDN de foony.io et tout standardisé sur foony.com, le support des proxies est devenu drastiquement plus simple :

  • Chargements d'assets en same-origin.
  • Moins de « cas spéciaux » à expliquer à un ServiceWorker de proxy.
  • Moins de réécriture.
  • Code moins fragile.

En bref, supprimer foony.io était une simplification architecturale qui a réduit la surface d'attaque pour les comportements bizarres de proxies.


Étape 3 : ce qui marche, ce qui ne marche pas, et pourquoi

Proxies confirmés fonctionnels

À ce stade, Foony fonctionne derrière :

  • croxyproxy
  • proxyorb

Quelques autres proxies fonctionnent probablement. Je parie que la plupart ne fonctionnent toujours pas. Mais au moins les importants que les gens utilisent pour jouer semblent fonctionner.

Pourquoi pas « tous les proxies » ?

Certains proxies ne peuvent tout simplement pas supporter une appli web multijoueur moderne. Exemples :

  • Proxies qui ne supportent pas correctement HTTPS.
  • Proxies qui cassent ou bloquent les WebSockets (Foony utilise du réseau temps réel). Techniquement on pourrait contourner ça, mais ça ajouterait de la complexité.
  • Proxies qui ont trop de restrictions sur les requêtes cross-origin, les en-têtes ou les ServiceWorkers.

Points clés à retenir

Les proxies web sont très peu sûrs

Ce sont des middlewares qui :

  • réécrivent le HTML
  • réécrivent le JavaScript
  • injectent parfois un ServiceWorker
  • et dépendent souvent des paramètres de requête / de l'encodage d'URL pour router les requêtes
  • peuvent bidouiller vos pages d'un nombre incalculable de façons

J'ai été surpris de voir jusqu'où certains proxies vont : ils réécrivent les chaînes sources de shaders, les commentaires, et Dieu sait quoi d'autre.

Parfois le meilleur correctif est architectural

Le patch WebGL a fait que les jeux s'affichaient à nouveau, mais supprimer la stratégie CDN multi-domaines a fait que le support des proxies reste stable.

C'est un bon rappel : les optimisations astucieuses peuvent être parfaitement raisonnables jusqu'à ce qu'elles entrent en collision avec un middleware hostile. Ou les extensions de navigateur de l'utilisateur. Ou Safari. Ou les paramètres de langue. Ou les fonctionnalités d'accessibilité. Ou les éruptions solaires. Ou n'importe quoi, vraiment.


Conclusion

Foony fonctionne maintenant derrière les proxies qui comptent (croxyproxy et proxyorb), sans transformer le code en un fouillis spécifique aux proxies :

  • Un correctif générique pour les shaders Three.js (pas d'identifiants spécifiques aux proxies).
  • Une stratégie de domaine plus simple (foony.com partout).
8 Ball Pool online multiplayer billiards icon