haxball.js
Version:
A powerful library for interacting with the Haxball Headless API
187 lines (151 loc) • 5.78 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
async function nodeify() {
try {
// Fetch cache hash
const hashResponse = await fetch('https://www.haxball.com/cache_hash.json');
if (!hashResponse.ok)
throw new Error(`Failed to fetch hash: ${hashResponse.status}`);
let hash = (await hashResponse.text()).replaceAll('"', '');
// Fetch source
const sourceUrl = `https://www.haxball.com/${hash}/__cache_static__/g/headless-min.js`;
const sourceResponse = await fetch(sourceUrl);
if (!sourceResponse.ok)
throw new Error(`Failed to fetch source: ${sourceResponse.status}`);
let source = await sourceResponse.text();
// Extract hash from source header comment
const hashMatch = source.match(/\/\*[\s\S]*?(\b[a-f0-9]{8}\b)/);
hash = hashMatch?.[1];
// Process source code
source = processSource(source);
// Write output
const outputPath = path.join(__dirname, '../src/build.js');
await writeOutputFile(outputPath, source);
console.log(`SUCCESS:${hash}`); // Special format for GitHub Actions
return hash;
} catch (error) {
console.error(`ERROR:${error.message}`);
process.exit(1);
}
}
function processSource(source) {
// Remove window references
source = removeWindowReferences(source);
// Apply regex replacements
source = applyRegexReplacements(source);
// Add proxy support
source = addProxySupport(source);
// Wrap with module code
return wrapWithModuleCode(source);
}
function removeWindowReferences(source) {
const replacements = {
'window.': '',
'parent.': '',
'document.': '',
'.innerHTML': '',
'getElementById("roomlink")': 'null',
'getElementById("recaptcha")': 'null',
};
return Object.entries(replacements).reduce(
(text, [search, replace]) => text.replaceAll(search, replace),
source
);
}
function applyRegexReplacements(source) {
// HBInit replacement
const hbInitMatch = source.match(/HBInit=.+?;/);
if (!hbInitMatch) throw new Error('Failed to find HBInit pattern');
source = source.replace(
hbInitMatch[0],
`promiseResolve(${hbInitMatch[0].substring(7, hbInitMatch[0].length - 1)});`
);
// WebSocket replacement
const wsMatch = source.match(/new WebSocket\([^)]+\);/);
if (!wsMatch) throw new Error('Failed to find WebSocket pattern');
source = source.replace(
wsMatch[0],
wsMatch[0].replace(
/new WebSocket\(([^)]+)\);/,
'new WebSocket($1, {headers:{origin: "https://html5.haxball.com"}, agent: proxyAgent});'
)
);
// Add WebSocket error debug
const wsErrorPattern =
/([a-zA-Z]+)\.([a-zA-Z]+)\.onerror=function\(\){([a-zA-Z]+)\.([a-zA-Z]+)\(!0\)}/;
const wsErrorMatch = source.match(wsErrorPattern);
if (!wsErrorMatch) throw new Error('Failed to find WebSocket error handler');
const [fullMatch, objName, wsProperty, methodObj, methodName] = wsErrorMatch;
const debugCode = `${objName}.${wsProperty}.onerror=function(err){${methodObj}.${methodName}(!0);debug && console.error(err)};`;
source = source.replace(fullMatch, debugCode);
// Recaptcha replacement
const recaptchaMatch = source.match(
/case "recaptcha":([a-zA-Z]+)\(([^)]+)\)/
);
if (!recaptchaMatch) throw new Error('Failed to find Recaptcha pattern');
source = source.replace(
recaptchaMatch[0],
'case "recaptcha":console.log(new Error("Invalid Token Provided!"))'
);
return source;
}
function addProxySupport(source) {
// Find the initialization pattern with more flexible matching
const initPattern =
/if\s*\([A-Za-z]+\.[A-Za-z]+\)\s*throw\s+[A-Za-z]+\.[A-Za-z]+\s*\(\s*"Can't init twice"\s*\)\s*;\s*[A-Za-z]+\.[A-Za-z]+\s*=\s*!0\s*;/;
const initMatch = source.match(initPattern);
if (!initMatch) {
throw new Error('Could not find initialization pattern for proxy support');
}
// Get the RoomConfigLookup function name
const configLookupMatch = source.match(/(\w+)\("noPlayer",/);
if (!configLookupMatch) {
throw new Error('Could not find RoomConfigLookup function');
}
const configFn = configLookupMatch[1];
// Add proxy support code after initialization
const proxyCode = `${initMatch[0].slice(
0,
-3
)}!0;proxyAgent = ${configFn}("proxy", null) ? new HttpsProxyAgent(url.parse(${configFn}("proxy", null))) : null; debug = ${configFn}("debug", null) == true;`;
// Replace the entire initialization block
source = source.replace(initMatch[0], proxyCode);
return source;
}
function wrapWithModuleCode(source) {
const header = `const WebSocket = require("ws");
const XMLHttpRequest = require("xhr2");
const JSON5 = require("json5");
const url = require("url");
const pako = require("pako");
const { HttpsProxyAgent } = require("https-proxy-agent");
const { Crypto } = require("@peculiar/webcrypto");
const { performance } = require("perf_hooks");
const crypto = new Crypto();
let { RTCPeerConnection, RTCIceCandidate, RTCSessionDescription } = require("@mertushka/node-datachannel/polyfill");
var promiseResolve;
var proxyAgent;
var debug = false;
const HBLoaded = (config) => {
if(config?.webrtc) {
RTCPeerConnection = config.webrtc.RTCPeerConnection;
RTCIceCandidate = config.webrtc.RTCIceCandidate;
RTCSessionDescription = config.webrtc.RTCSessionDescription;
}
return new Promise(function (resolve, reject) {
promiseResolve = resolve;
});
}
const onHBLoaded = function (cb) {
return cb;
};
/* Builded & Automated with Haxball.JS Nodeify Script */
`;
const footer = `\nmodule.exports = HBLoaded;`;
return header + source + footer;
}
async function writeOutputFile(outputPath, content) {
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, content, 'utf8');
}
nodeify();