background blurbackground mobile blur

1/1/1970

Comment j'ai réussi à faire tourner Foony derrière des proxies web

Salut ! Je sais depuis longtemps que les proxies web peuvent créer des problèmes de compatibilité pour les sites. Par contre, le support de Foony derrière des proxies était notoirement mauvais, et régler la compatibilité de Foony avec les proxies a été plutôt délicat.

Ce n'est pas non plus un problème du genre « Foony utilise des API exotiques » (même si c'est un peu le cas). C'était plutôt une combinaison de :

  • Proxies qui réécrivent agressivement des chaînes de caractères à des endroits où ils ne devraient surtout pas le faire.
  • Proxies qui traitent le domaine du site principal différemment des « autres » domaines (CDN, hébergeurs de fichiers, etc.).
  • Et la dure réalité que certains proxies ne peuvent tout simplement pas supporter les applis web modernes (HTTPS correct, WebSockets, etc.).

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

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


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

Le symptôme

Quand j'ai essayé croxyproxy, je n'ai pas pu jouer à 8 Ball Pool ni aux autres jeux three.js disponibles sur Foony.
J'avais sans arrêt une erreur de compilation de shader dans Three.js avec des messages du genre :

  • « Shader Error 1282 - VALIDATE_STATUS false »

Ce message ne servait quasiment à rien. En gros, ça veut dire « ton shader est invalide, débrouille-toi ». Super. Si tu te demandes pourquoi j'utilise toujours des messages d'erreur uniques pour chaque erreur sur Foony, c'est à cause de ça. Ça aide à vraiment cibler les problèmes au lieu d'un vague « le code est cassé, va le réparer ».

Mais pourquoi des shaders three.js parfaitement valides se mettaient à planter ? Qu'est‑ce qui clochait ?

La vraie cause : des proxies qui abîment layout(location = N)

Three.js génère du GLSL avec des qualifiers 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 de chaîne global bien naïf. C'est déjà une mauvaise idée en JS, mais en plus ils le faisaient dans les chaînes de texte des shaders. J'imagine que parser un AST coûte trop cher pour eux.

Du coup, la source du shader se retrouvait corrompue et devenait quelque chose comme :

layout(__cpLocation = 0) in vec3 position;

L'identifiant doit absolument être location à cet endroit. Tout le reste est du GLSL invalide, et le compilateur le rejette. (Layout Qualifiers in GLSL)

C'est un problème Three.js uniquement dans le sens où Three.js génère les shaders dynamiquement et qu'on les passe à WebGL à l'exécution. 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 croxyproxy pour location, à savoir __cpLocation, et de la remplacer par location. Sauf que différents proxies utilisent différents noms de remplacement. Certains utilisent __cpLocation, d'autres des identifiants bizarres différents. Donc coder en dur un correctif du type « remplacer __cpLocation par location » serait très fragile.

J'avais besoin :

  • D'un correctif générique (sans identifiants spécifiques à un proxy).
  • D'une solution qui fonctionne 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, c'est de ne simplement pas utiliser location. Facile. J'avais déjà vu des astuces du genre en Lua en rétro‑ingéniant le système de protection de guides de RestedXP (si je me souviens bien, ils ofbusquent leur usage de BNGetInfo, par exemple _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).

Cette astuce marche aussi en JavaScript, évidemment. Dans client/index.html, je décode ceci à l'exécution :

// 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

Ce atob() est exécuté après que le proxy a déjà fait sa réécriture HTML/JS, donc il ne peut pas « pré‑corrompre » la chaîne. Je découpe la chaîne en deux pour la rendre encore plus difficile à détecter, et j'utilise 'atob' parce que je peux, mais String.fromCharCode ou un échappement hexadécimal sur window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] fonctionnerait probablement aussi.

Le motif du shader cassé est toujours structurellement le même :

layout(<something> = <number>)

Donc je matche ça de façon générique et je remplace <something> par le bon identifiant :

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

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

Une fois ça en place, les erreurs de compilation de shader ont disparu. À ce stade, croxyproxy fonctionnait, mais proxyorb plantait toujours. Pourquoi ?! Ça ne devrait pas marcher pareil ?


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

Historiquement, Foony utilisait deux domaines, au moins depuis le mois dernier :

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

La raison de départ était pratique : servir les assets depuis un domaine sans cookies évite d'envoyer des en‑têtes de cookies inutiles à chaque requête de fichier statique. C'est chouette, mais pas aussi indispensable qu'on pourrait le croire, sachant qu'HTTP/2 utilise HPACK pour réduire la taille des en‑têtes envoyés sur le fil.

C'est une optimisation tout à fait valable en navigation normale.

Derrière des proxies, c'est devenu une grosse source de casse. Et les joueurs Foony adorent les proxies. soupir

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

Beaucoup de proxies sont optimisés pour « proxyfier cette page / ce domaine précis ». Ils chargent correctement le HTML principal, injectent leurs scripts, enregistrent leur propre ServiceWorker, etc.

Mais quand l'appli commence à récupérer des fichiers depuis une origine différente (par exemple foony.io), on tombe vite sur toute une collection de bugs amusants :

  • Des échecs d'interception par le ServiceWorker, du genre :
    • « ServiceWorker intercepted the request and encountered an unexpected error »
    • « Loading failed for the module with source »
  • Des paramètres de requête exigés par l'infrastructure du proxy (et faciles à enlever par erreur).
  • Des requêtes de fichiers qui perdent les métadonnées de routage internes du proxy.
  • Des proxies bizarres qui remplacent toute la requête par generic-php-slug.php?someQueryParam=hugeEncodedString (oui, je n'ai même pas essayé de supporter ça).

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

Un exemple révélateur était une URL de fichier du genre :

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

Ce ?__pot=... est l'état / le routage propre au proxy, qui lui indique pour quel domaine la requête est destinée. Si tu le retires, les proxies ne peuvent plus résoudre correctement la requête, et tu te retrouves sur le chemin d'erreur du ServiceWorker.

Le « remplacement de ressources » à la rescousse (et pourquoi ça s'est compliqué très vite)

À un moment, j'ai tenté un contournement : détecter « on est derrière un proxy », puis remplacer toutes les URLs de ressources foony.io par l'origine actuelle, pour que le proxy voie tout comme du même domaine.

Sur le papier, ça paraît raisonnable, et ça marchait pour croxyproxy, mais ça ajoutait énormément de complexité :

  • Il faut remplacer les balises link et script déjà présentes dans le HTML.
  • Il faut un MutationObserver pour gérer les balises injectées dynamiquement (modulepreload, feuilles de style, etc.).
  • Il faut préserver les paramètres de requête du proxy, sinon tu casses leur routage. Et chaque proxy fait ça à sa façon. Évidemment.
  • Et il faut quand même garder une logique générique (aucune globale spécifique à un proxy) pour éviter que le code ne devienne une grosse décharge infâme.

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

Rétro‑ingénierie du script injecté par CroxyProxy

À ce stade, j'ai commencé à être curieux : qu'est‑ce que le proxy fait vraiment à ma page ? Il injecte ses propres pubs ? Pire ?

Le script côté client de CroxyProxy est fortement ofbusqué.

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

Ce qui, une fois exécuté, donne :

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

D'après ça, on dirait que croxyproxy utilise Obfuscator.io pour cette ofbuscation. Heureusement, c'est assez facile à déofbusquer avec webcrack.

On obtient alors un JavaScript beaucoup 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)) {

Sympa. Maintenant on peut voir ce que ça fait. Et… ça a l'air globalement correct. Je pense que l'ofbuscation sert surtout à rendre le proxy plus difficile à détecter. Surtout.

On a une partie injection de pubs / d'interface :

    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 d'autres endroits, mais en gros ça affiche des pubs, y compris des pop‑under. Ça utilise aussi FuckAdBlock.

Le vrai remplacement de chaînes, par contre, se fait côté serveur. Et là, Dieu seul sait tout ce que ça fait.

Dans tous les cas, tu ne devrais absolument pas utiliser de proxies web si tu tiens à la sécurité de ton compte. Si tu n'as vraiment pas le choix, évite d'y saisir tes infos personnelles / de compte / de paiement.

Le « remplacement de ressources » à la poubelle

J'ai fini par décider que la complexité liée au remplacement de ressources, combinée à la complexité dans d'autres parties du code pour supporter foony.io, ne valait pas les petits gains réseau de jolies requêtes sans cookies. On constatait aussi une baisse inexpliquée des conversions vers le gameplay depuis l'adoption de foony.io, donc je soupçonne qu'il y avait d'autres problèmes liés à foony.io qu'on n'avait pas encore identifiés.

Donc j'ai retiré foony.io. Au moins pour l'instant.

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

  • Chargements de fichiers en même origine.
  • Moins de « cas particuliers » à expliquer au ServiceWorker du proxy.
  • Moins de réécriture.
  • Un code moins fragile.

En bref, retirer foony.io a simplifié l'architecture et réduit la surface sur laquelle les proxies pouvaient partir en vrille.


Passe 3 : ce qui marche, ce qui ne marche pas, et pourquoi

Proxies qui fonctionnent

À ce stade, Foony fonctionne derrière :

  • croxyproxy
  • proxyorb

D'autres proxies fonctionnent probablement. Je parierais que la plupart ne fonctionnent toujours pas. Mais au moins, les principaux que les gens utilisent pour jouer ont l'air de marcher.

Pourquoi pas « tous les proxies » ?

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

  • Des proxies qui ne gèrent pas correctement HTTPS.
  • Des proxies qui cassent ou bloquent les WebSockets (Foony utilise du réseau en temps réel). Techniquement, on pourrait contourner ça, mais ça ajouterait pas mal de complexité.
  • Des proxies avec trop de restrictions sur les requêtes cross‑origin, les en‑têtes ou les ServiceWorkers.

Points clés

Les proxies web sont vraiment peu sécurisés

Ce sont des middlewares qui :

  • réécrivent le HTML
  • réécrivent le JavaScript
  • injectent parfois un ServiceWorker
  • dépendent souvent de paramètres de requête / d'encodage d'URL pour router les requêtes
  • peuvent bidouiller tes pages de mille façons différentes

J'ai été surpris de voir à quel point certains proxies vont loin : ils réécrivent même les chaînes de texte des shaders, les commentaires, et sans doute plein d'autres trucs.

Parfois, la meilleure solution est architecturale

Le patch WebGL a permis aux jeux de s'afficher à nouveau, mais c'est l'abandon de la stratégie CDN multi‑domaine qui a rendu le support des proxies durablement stable.

C'est un bon rappel : certaines optimisations malines peuvent être totalement raisonnables… jusqu'à ce qu'elles se cognent à un middleware hostile. Ou aux extensions du navigateur des utilisateurs. Ou à Safari. Ou aux réglages de langue. Ou aux options d'accessibilité. Ou aux éruptions solaires. Ou à n'importe quoi, en fait.


Conclusion

Foony fonctionne maintenant derrière les proxies qui comptent (croxyproxy et proxyorb), sans transformer la base de code en enfer spécifique aux proxies :

  • Un correctif générique pour les shaders Three.js (sans identifiants propres à un proxy).
  • Une stratégie de domaines plus simple (foony.com partout).
8 Ball Pool online multiplayer billiards icon