

1/1/1970
Πώς έκανα το Foony να δουλεύει πίσω από proxies
Γεια χαρά! Ξέρω εδώ και πολύ καιρό ότι τα web proxies προκαλούν προβλήματα συμβατότητας σε ιστοσελίδες. Όμως, η υποστήριξη του Foony στα proxies ήταν διαβόητα κακή, και η λύση της συμβατότητας με proxies αποδείχτηκε αρκετά μπερδεμένη.
Δεν είναι ότι το Foony χρησιμοποιεί κάποια εξωτικά API (αν και όντως το κάνουμε). Ήταν ένας συνδυασμός από:
- Proxies που κάνουν επιθετική αντικατάσταση συμβολοσειρών εκεί που σίγουρα δεν έπρεπε.
- Proxies που αντιμετωπίζουν το κύριο domain του site διαφορετικά από τα "άλλα" domains (CDNs, asset hosts, κ.λπ.).
- Και τη σκληρή πραγματικότητα ότι κάποια proxies απλώς δεν μπορούν να υποστηρίξουν σύγχρονες web εφαρμογές (σωστή υποστήριξη HTTPS, WebSockets, κ.λπ.).
Δεν δουλεύουμε με κάθε proxy, αλλά πλέον δουλεύουμε τουλάχιστον με το croxyproxy και το proxyorb, κάτι που ήταν και ο στόχος.
Παρακάτω εξηγώ τι χάλασε, γιατί χάλασε, και ποιες διορθώσεις πραγματικά είχαν σημασία.
Πέρασμα 1: Έγκυρα αλλά σπασμένα Three.js shaders
Το σύμπτωμα
Όταν δοκίμασα το croxyproxy, δεν μπορούσα να παίξω 8 Ball Pool ή κανένα από τα άλλα three.js παιχνίδια του Foony. Έπαιρνα συνέχεια αποτυχία compilation του shader στο Three.js, με σφάλματα όπως:
- "Shader Error 1282 - VALIDATE_STATUS false"
Αυτό το μήνυμα ήταν σχεδόν εντελώς άχρηστο. Συνήθως σημαίνει "ο shader σου είναι μη έγκυρος, καλή τύχη". Υπέροχα. Αν αναρωτιέστε γιατί χρησιμοποιώ πάντα μοναδικά μηνύματα σφάλματος για κάθε σφάλμα στο Foony, να γιατί. Βοηθάει στον εντοπισμό προβλημάτων αντί για ένα απλό "ο κώδικας χάλασε, πήγαινε διόρθωσέ τον".
Αλλά γιατί χαλούσαν τέλεια έγκυροι shaders του three.js; Τι παίζει;
Η πραγματική αιτία: proxies που χαλάνε το layout(location = N)
Το Three.js παράγει GLSL με layout qualifiers όπως:
layout(location = 0) in vec3 position;
Κάποια proxies προσπαθούν να ξαναγράψουν οτιδήποτε μοιάζει με το JavaScript API location κάνοντας αφελή global αντικατάσταση συμβολοσειρών. Αυτό είναι ήδη κακό στη JS, αλλά το έκαναν και μέσα σε source strings shader. Φαντάζομαι το AST parsing είναι πολύ ακριβό για αυτούς.
Έτσι, ο κώδικας του shader διαφθειρόταν σε κάτι σαν:
layout(__cpLocation = 0) in vec3 position;
Το αναγνωριστικό πρέπει να είναι location εκεί. Οτιδήποτε άλλο είναι μη έγκυρο GLSL, και ο compiler το απορρίπτει. (Layout Qualifiers in GLSL)
Αυτό είναι πρόβλημα του Three.js μόνο με την έννοια ότι το Three.js παράγει shaders δυναμικά, και τους περνάμε στο WebGL κατά τη διάρκεια εκτέλεσης. Το πραγματικό bug είναι η στρατηγική αντικατάστασης του proxy.
Γιατί δεν "διόρθωσα το proxy"
Μια αφελής προσέγγιση θα ήταν να ψάξω για τη συμβολοσειρά αντικατάστασης του location στο croxyproxy, το __cpLocation, και να την αντικαταστήσω με location. Όμως, διαφορετικά proxies χρησιμοποιούν διαφορετικά ονόματα αντικατάστασης. Κάποια χρησιμοποιούν __cpLocation, άλλα χρησιμοποιούν άλλα περίεργα αναγνωριστικά. Οπότε το να κάνεις hardcode μια διόρθωση τύπου "αντικατέστησε το __cpLocation πίσω σε location" είναι εύθραυστο.
Χρειαζόμουν:
- Μια γενική λύση (χωρίς hardcoding αναγνωριστικών proxy).
- Μια λύση που να δουλεύει ακόμα και αν το proxy ξαναγράφει τη λέξη
locationκαι μέσα στη JavaScript μου.
Το κόλπο με το base64: κρύβοντας τη λέξη location από το proxy
Αν το proxy αντικαθιστά κάθε literal location που βλέπει, η πιο απλή κίνηση είναι απλώς να μη χρησιμοποιείς location. Αρκετά εύκολο. Έχω δει τέτοια κόλπα στο παρελθόν στη Lua όταν έκανα reverse-engineering στο σύστημα προστασίας οδηγών της RestedXP (αν θυμάμαι σωστά, αποκρύπτουν τη χρήση του BNGetInfo, π.χ. _G("\x42\x4E\x47\x65\x74\x49\x6E\x66\x6F")).
Αυτό το κόλπο φυσικά δουλεύει και στη JavaScript. Στο client/index.html, αποκωδικοποιώ το παρακάτω κατά τη διάρκεια εκτέλεσης:
// Επειδή αυτά τα proxies προσπαθούν να αντικαταστήσουν κάθε `location`, χρησιμοποιούμε μια συμβολοσειρά κωδικοποιημένη σε base64.
const suffix = 'pb24=';
const locStr = atob('bG9jYXR' + suffix); // "location"
const loc = window[locStr]; // window.location
Αυτό το atob() συμβαίνει μετά που το proxy έχει ήδη κάνει την αντικατάσταση HTML/JS, οπότε δεν μπορεί να "προ-διαφθείρει" τη συμβολοσειρά. Σπάω τη συμβολοσειρά σε δύο για να γίνει ακόμα πιο δύσκολο να ανιχνευτεί, και χρησιμοποιώ 'atob' επειδή μπορώ, αλλά το String.fromCharCode ή το hex-escaping window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e'] ίσως επίσης να δουλεύουν.
Το μοτίβο του χαλασμένου shader είναι πάντα δομικά το ίδιο:
layout(<κάτι> = <αριθμός>)
Οπότε το ταιριάζω γενικά και αντικαθιστώ το <κάτι> με το σωστό αναγνωριστικό:
source.replace(/layout\s*\(\s*[^=)]+\s*=\s*(\d+)\s*\)/g, 'layout(' + locStr + ' = $1)');
Το hook στο WebGL: patch στο shaderSource (WebGL1 + WebGL2)
Επειδή το Three.js καλεί το gl.shaderSource(shader, source), κάνω patch στο ίδιο το 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,
});
Και εφαρμόζω το ίδιο patch στο WebGL2RenderingContext αν υπάρχει.
Μόλις μπήκε αυτό στη θέση του, τα σφάλματα compilation των shaders εξαφανίστηκαν. Σε αυτό το σημείο, το croxyproxy δούλευε αλλά το proxyorb συνέχιζε να αποτυγχάνει. Γιατί;! Δε θα έπρεπε να δουλεύει με τον ίδιο τρόπο;
Πέρασμα 2: Το δεύτερο πρόβλημα (domains) και γιατί η αφαίρεση του foony.io έκανε τα πάντα πιο εύκολα
Το Foony ιστορικά χρησιμοποιούσε δύο domains, τουλάχιστον τον τελευταίο μήνα:
foony.comγια το κύριο sitefoony.ioγια στατικά assets
Ο αρχικός λόγος ήταν πρακτικός: το να σερβίρεις assets από ένα domain χωρίς cookies αποφεύγει το πρήξιμο των cookie headers σε κάθε αίτημα στατικού αρχείου. Αυτό είναι υπέροχο, αλλά όχι τόσο απαραίτητο όσο θα νόμιζες, δεδομένου ότι το HTTP/2 χρησιμοποιεί HPACK για να μειώσει τα bytes που στέλνονται για headers.
Είναι μια έγκυρη βελτιστοποίηση σε κανονική περιήγηση.
Πίσω από proxies, έγινε μια κύρια πηγή σπασιμάτων. Και η βάση χρηστών του Foony λατρεύει τα proxies. Αναστεναγμός.
Τα proxies αντιμετωπίζουν το "κύριο site" διαφορετικά από τα "άλλα sites"
Πολλά proxies είναι βελτιστοποιημένα για "proxy αυτή τη μία σελίδα / domain". Θα φορτώσουν επιτυχώς το κύριο HTML, θα εισάγουν scripts, θα καταχωρήσουν δικό τους ServiceWorker, κ.λπ.
Αλλά όταν η εφαρμογή ξεκινά να τραβάει assets από ένα διαφορετικό origin (όπως το foony.io), μπαίνεις σε κάθε λογής διασκεδαστικά σπασίματα:
- Αποτυχίες παρεμβολής ServiceWorker όπως:
- "ServiceWorker intercepted the request and encountered an unexpected error"
- "Loading failed for the module with source"
- Query params που απαιτούνται από την υποδομή του proxy (και είναι εύκολο να αφαιρεθούν κατά λάθος).
- Αιτήματα assets που χάνουν τα εσωτερικά μεταδεδομένα δρομολόγησης του proxy.
- Παράξενα proxies που αντικαθιστούν ολόκληρο το αίτημα με κάτι σαν
generic-php-slug.php?someQueryParam=hugeEncodedString(ναι, δεν μπήκα στον κόπο να το υποστηρίξω αυτό).
Οι εσωτερικοί μηχανισμοί αυτών των proxy εξαρτώνται από query params / URL encoding, και είναι αρκετά εύθραυστοι.
Ένα από τα χαρακτηριστικά παραδείγματα ήταν ένα URL asset σαν:
https://<proxy-ip>/assets/firebase-<hash>.js?__pot=aHR0cHM6Ly9mb29ueS5jb20
Αυτό το ?__pot=... είναι η δική του δρομολόγηση/κατάσταση του proxy που λέει στο proxy για ποιο domain είναι το αίτημα. Αν το αφαιρέσεις, τα proxies δεν μπορούν να επιλύσουν σωστά το αίτημα, και καταλήγεις στο μονοπάτι σφάλματος του ServiceWorker.
"Resource swapping" προς διάσωση (και γιατί έγινε γρήγορα πολύπλοκο)
Σε κάποιο σημείο, δοκίμασα μια λύση: εντοπίζω "είμαστε σε proxy", και μετά αντικαθιστώ τα URLs πόρων του foony.io στο τρέχον origin ώστε το proxy να βλέπει τα πάντα ως same-origin.
Αυτό ακούγεται λογικό, και δούλευε για το croxyproxy, αλλά πρόσθεσε πολλή πολυπλοκότητα:
- Πρέπει να αντικαταστήσεις tags
linkκαιscriptπου υπάρχουν ήδη στο HTML. - Χρειάζεσαι έναν
MutationObserverγια να χειριστείς δυναμικά εισαγόμενα tags (modulepreload, stylesheet, κ.λπ.). - Πρέπει να διατηρείς τα query parameters του proxy, αλλιώς σπας τη δρομολόγησή του. Και διαφορετικά proxies το κάνουν αυτό διαφορετικά. Επειδή φυσικά.
- Και πρέπει ακόμα να κρατήσεις τη λογική γενική (χωρίς proxy-specific globals) ώστε ο κώδικας να μη γίνει ένας φουσκωμένος σκουπιδότοπος.
Εδώ ξαναεμφανίστηκε και το "κόλπο με το base64": ακόμα και στη δική μου JavaScript, έπρεπε να είμαι προσεκτικός με τη literal συμβολοσειρά location επειδή το proxy μπορεί να την ξαναγράψει.
Reverse-engineering στο εισαγόμενο script του CroxyProxy
Σε αυτό το σημείο μου μπήκε η περιέργεια: τι κάνει στ' αλήθεια το proxy στη σελίδα μου; Εισάγει τις δικές του διαφημίσεις; Κάτι χειρότερο;
Το client-side script του CroxyProxy είναι έντονα obfuscated.
(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)) {
Ωραία. Τώρα μπορούμε να δούμε τι κάνει. Και... φαίνεται κυρίως μια χαρά. Νομίζω ότι η απόκρυψη είναι κυρίως για να βοηθήσει στην αποτροπή ανίχνευσης του proxy. Κυρίως.
Έχουμε λίγο ad / UI injection:
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.
Η πραγματική αντικατάσταση των συμβολοσειρών, όμως, γίνεται από την πλευρά του server. Και ποιος ξέρει τι όλα αυτά κάνει.
Είτε έτσι είτε αλλιώς, σίγουρα δεν πρέπει να χρησιμοποιείτε web proxies αν σας νοιάζει η ασφάλεια του λογαριασμού σας. Αν πρέπει να το κάνετε, αποφύγετε να εισάγετε οποιαδήποτε από τις προσωπικές σας πληροφορίες / λογαριασμού / αγορών.
"Resource swapping" στα σκουπίδια
Αποφάσισα ότι η πολυπλοκότητα από το resource swapping, μαζί με την πολυπλοκότητα σε άλλα μέρη του κώδικα για την υποστήριξη του foony.io, δεν άξιζε τη μικρή εξοικονόμηση δικτύου από όμορφα αιτήματα χωρίς cookies. Βλέπαμε επίσης μια ανεξήγητη πτώση στις μετατροπές του gameplay μας από τότε που υιοθετήσαμε το foony.io, οπότε υποπτεύομαι ότι υπήρχαν και άλλα ζητήματα με το foony.io που δεν γνωρίζαμε.
Έτσι, αφαίρεσα το foony.io. Τουλάχιστον προς το παρόν.
Μόλις διέγραψα τη λογική CDN του foony.io και τυποποίησα τα πάντα στο foony.com, η υποστήριξη proxy έγινε δραματικά πιο απλή:
- Φορτώσεις assets same-origin.
- Λιγότερες "ειδικές περιπτώσεις" να εξηγήσω σε ένα ServiceWorker proxy.
- Λιγότερη αντικατάσταση.
- Λιγότερο εύθραυστος κώδικας.
Με λίγα λόγια, η αφαίρεση του foony.io ήταν μια αρχιτεκτονική απλοποίηση που μείωσε την επιφάνεια για περίεργη συμπεριφορά proxy.
Πέρασμα 3: Τι δουλεύει, τι δε δουλεύει, και γιατί
Επιβεβαιωμένα proxies που δουλεύουν
Σε αυτό το σημείο, το Foony δουλεύει πίσω από:
- croxyproxy
- proxyorb
Κάποια άλλα proxies πιθανότατα δουλεύουν. Στοιχηματίζω ότι τα περισσότερα ακόμα δε δουλεύουν. Αλλά τουλάχιστον τα σημαντικά που χρησιμοποιεί ο κόσμος για να παίζει παιχνίδια φαίνεται να δουλεύουν.
Γιατί όχι "όλα τα proxies";
Κάποια proxies απλώς δεν μπορούν να υποστηρίξουν μια σύγχρονη πολυχρηστική web εφαρμογή. Παραδείγματα:
- Proxies που δεν υποστηρίζουν σωστά HTTPS.
- Proxies που σπάνε ή μπλοκάρουν WebSockets (το Foony χρησιμοποιεί δικτύωση πραγματικού χρόνου). Τεχνικά θα μπορούσες να το παρακάμψεις, αλλά θα πρόσθετε πολυπλοκότητα.
- Proxies που έχουν πάρα πολλούς περιορισμούς γύρω από cross-origin αιτήματα, headers, ή ServiceWorkers.
Βασικά συμπεράσματα
Τα web proxies είναι πολύ ανασφαλή
Είναι middleware που:
- ξαναγράφει HTML
- ξαναγράφει JavaScript
- μερικές φορές εισάγει ένα ServiceWorker
- και συχνά εξαρτάται από query params / URL encoding για να δρομολογήσει αιτήματα
- μπορεί να παίξει με τις σελίδες σου με κάθε δυνατό τρόπο
Εκπλάχθηκα με το πόσο βαθιά πάνε κάποια proxies: θα ξαναγράψουν source strings shader, σχόλια, και ένας Θεός ξέρει τι άλλο.
Μερικές φορές η καλύτερη λύση είναι αρχιτεκτονική
Το patch του WebGL έκανε τα παιχνίδια να ξαναπαίζουν, αλλά η αφαίρεση της στρατηγικής CDN πολλαπλών domains έκανε την υποστήριξη proxy να μείνει σταθερή.
Είναι μια καλή υπενθύμιση: οι έξυπνες βελτιστοποιήσεις μπορούν να είναι απόλυτα λογικές μέχρι να συγκρουστούν με εχθρικό middleware. Ή με τις επεκτάσεις περιηγητή του χρήστη. Ή με τον Safari. Ή με τις ρυθμίσεις γλώσσας. Ή με χαρακτηριστικά προσβασιμότητας. Ή με ηλιακές εκλάμψεις. Ή με οτιδήποτε, στ' αλήθεια.
Συμπέρασμα
Το Foony τώρα δουλεύει πίσω από τα proxies που έχουν σημασία (croxyproxy και proxyorb), χωρίς να μετατρέπει τη βάση κώδικα σε ένα proxy-specific χάος:
- Μια γενική διόρθωση για shaders του Three.js (χωρίς proxy-specific αναγνωριστικά).
- Μια απλούστερη στρατηγική domain (foony.com παντού).