Skip to content

Instantly share code, notes, and snippets.

@TheUltDev
Last active March 30, 2020 07:40
Show Gist options
  • Save TheUltDev/d29e5741d6057f5de03378a7e13cfab1 to your computer and use it in GitHub Desktop.
Save TheUltDev/d29e5741d6057f5de03378a7e13cfab1 to your computer and use it in GitHub Desktop.
"use strict";
const https = require('https');
const http = require('http');
const fs = require('fs');
const glob = require('glob');
const git = require('nodegit');
const path = require('path');
const rimraf = require('rimraf');
const readMultipleFiles = require('read-multiple-files');
const spawn = require('child_process').spawn;
const parseString = require('xml2js').parseString;
const createDownload = require('mt-downloader').createDownload
const patchList = require('./patches');
const UPDATE_CHECK_INTERVAL = 1 * 60 * 1000;
const CLIENT_BASE_URL = 'https://secure.tibia.com/flash-regular-bin';
const GIT_ORIGIN = 'https://github.com/user/repo-name.git';
const GIT_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const CLIENT_VERSION_REGEX = /CLIENT_VERSION:uint\s?=[^]*?(\d+);/;
const PROTOCOL_VERSION_REGEX = /PROTOCOL_VERSION:int\s?=[^]*?(\d+);/;
const CATALOG_REGEX = /<url>([^]*?)<\/url>/gm;
const OPTIONS_API = 'https://example.com/api/account/flash_options';
const FLASH_ROOT = '/client/flash';
const DEFAULT_OPTIONS_TPL = fs.readFileSync('./client-default-options.xml').toString();
const CATALOG_CLIENT_TPL = fs.readFileSync('./catalog-client-template.xml').toString();
const swfFiles = glob.sync('./clients/*.swf');
let pollClientInterval;
function _exists(path) {
try {
return fs.statSync(path).isFile();
} catch (err) {
return false;
}
}
function _exec(cmd, args, opt) {
const isWindows = /win/.test(process.platform);
if (isWindows) {
if (!args) {
args = [];
}
args.unshift(cmd);
args.unshift('/c');
cmd = process.env.comspec;
}
return spawn(cmd, args, opt);
}
function _findIndexes() {
}
function startClientUpdater() {
console.log('Monitoring tibia.com for flash client update...')
pollClientInterval = setInterval(pollClient, UPDATE_CHECK_INTERVAL);
pollClient();
}
function stopClientUpdater() {
if (pollClientInterval) {
clearInterval(pollClientInterval);
}
pollClientInterval = null;
}
function pollClient() {
const timestamp = process.hrtime()[1];
const response = [];
return https.get(`${CLIENT_BASE_URL}/catalog-client.xml?_${timestamp}`, (res) => {
res.on('data', chunk => response.push(chunk))
res.on('end', () => parseString(response.join(''), (err, result) => {
let swfUrl;
try { swfUrl = result.catalog.binary[0].url[0]; } catch(e) {}
if (swfUrl) {
const swfName = path.parse(swfUrl).base;
if (swfFiles.indexOf(`./clients/${swfName}`) === -1) {
console.log('Downloading flash client update...')
stopClientUpdater();
downloadClient(swfUrl, swfName, startClientUpdater);
}
}
}));
});
}
function downloadClient(swfUrl, swfName, callback) {
const output = fs.createWriteStream(`./clients/${swfName}`);
http.get(swfUrl, (res) => {
res.pipe(output);
res.on('end', () => {
patchClient(swfName, (protocol, version) => {
/*decompileClient(swfName, () => {
commitDecompiledClient(swfName, protocol, version, callback);
});*/
});
});
});
}
function downloadAssets(clientProtocol, clientVersion, callback) {
console.log('Downloading assets...');
const timestamp = process.hrtime()[1];
const response = [];
const downloads = [];
const contentDir = 'content';
const clientDir = `./clients/${clientProtocol}`;
const clientSwf = `${clientDir}/${clientVersion}.swf`;
const outputDir = `${clientDir}/${contentDir}`;
let contents;
if (!fs.existsSync(outputDir))
fs.mkdirSync(outputDir);
return https.get(`${CLIENT_BASE_URL}/catalog-content.xml?_${timestamp}`, (res) => {
res.on('data', chunk => {response.push(chunk)})
res.on('end', () => {
contents = response.join('');
const matches = contents.match(CATALOG_REGEX);
if (matches) {
matches.forEach((match) => {
const url = match.replace('<url>', '').replace('</url>', '');
const path = url.replace('http://static.tibia.com/flash-regular-data', outputDir);
// Missing, download from CipSoft
if (!_exists(path)) {
downloads.push(createDownload({path, url}).start().toPromise());
}
});
}
Promise.all(downloads).then(() => {
contents = contents.split('http://static.tibia.com/flash-regular-data').join(`${FLASH_ROOT}/${contentDir}`);
fs.writeFileSync(`${clientDir}/client-default-options.xml`, DEFAULT_OPTIONS_TPL);
fs.writeFileSync(`${clientDir}/catalog-content.xml`, contents);
const clientCatalogContents = CATALOG_CLIENT_TPL
.replace('{{CLIENT_SIZE}}', fs.statSync(clientSwf).size)
.replace('{{CLIENT_URL}}', `${FLASH_ROOT}/${clientVersion}.swf`)
.replace('{{DEFAULT_OPTIONS_SIZE}}', DEFAULT_OPTIONS_TPL.length)
.replace('{{DEFAULT_OPTIONS_URL}}', `${FLASH_ROOT}/client-default-options.xml`)
.replace('{{OPTIONS_API}}', OPTIONS_API);
fs.writeFileSync(`${clientDir}/catalog-client.xml`, clientCatalogContents);
callback();
});
});
});
}
function patchClient(swfName, callback) {
console.log('Patching flash client...')
const scripts = ['tibia.network.Communication'];
const paths = ['./cache/patches/scripts/tibia/network/Communication.as'];
const references = ['./cache/patches/scripts/tibia/network/Communication.pcode'];
patchList.forEach(patch => {
scripts.push(patch.class);
paths.push(patch.path);
references.push(patch.reference || null);
});
const parseScripts = _exec('java', [
'-jar', './bin/ffdec.jar',
'-selectclass', scripts.join(','),
'-format', 'script:as3',
'-config', 'showMethodBodyId=1',
'-export', 'script', './cache/patches/',
`./clients/${swfName}`
]);
// Output AS3 scripts
parseScripts.on('close', (code, signal) => {
if (code === 0) {
parseScripts.kill();
readMultipleFiles(paths, 'utf8', (err, scriptContents) => {
if (err) {
throw err;
}
const communications = scriptContents[0];
const clientProtocol = communications.match(PROTOCOL_VERSION_REGEX)[1];
const clientVersion = communications.match(CLIENT_VERSION_REGEX)[1];
const outputDir = `./clients/${clientProtocol}`;
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
const referencePcode = _exec('java', [
'-jar', './bin/ffdec.jar',
'-selectclass', scripts.join(','),
'-format', 'script:pcode',
'-config', 'showMethodBodyId=1',
'-export', 'script', './cache/patches/',
`./clients/${swfName}`
]);
// Output reference pcode
referencePcode.on('close', (code, signal) => {
if (code === 0) {
referencePcode.kill();
readMultipleFiles(references, 'utf8', (err, referenceContent) => {
const instructions = [
'-jar', './bin/ffdec.jar',
'-replace', `./clients/${swfName}`, `${outputDir}/${clientVersion}.swf`
];
patchList.forEach((patch, index) => {
const classIndex = paths.indexOf(patch.path);
const classContents = scriptContents[classIndex];
const methodBodyIndex = classContents.match(patch.regex);
// Found body method index
if (methodBodyIndex) {
// No references needed, use instructions as-is
if (!patch.pcodeRegex) {
instructions.push(patch.class);
instructions.push(`./patches/${patch.pcode}`);
instructions.push(methodBodyIndex[1]);
// Reference original pcode for specific variables
} else {
const pcodeContents = referenceContent[classIndex];
const pcodeRefMatches = pcodeContents.match(patch.pcodeRegex);
// Replace variables with matched regex
if (pcodeRefMatches && pcodeRefMatches.length > 1) {
const customPcode = fs.readFileSync(`./patches/${patch.pcode}`).toString();
const pcodePatchPath = `./cache/patches/${patch.pcode}`;
let modifiedPcode = customPcode;
for (var i = 1; i < pcodeRefMatches.length; i++) {
modifiedPcode = modifiedPcode.split(`{{${i}}}`).join(pcodeRefMatches[i]);
}
fs.writeFile(pcodePatchPath, modifiedPcode);
instructions.push(patch.class);
instructions.push(pcodePatchPath);
instructions.push(methodBodyIndex[1]);
// Did not find any references, we were expecting them
} else {
throw new Error(`Failed to find pcode references: ${patch.class}`);
}
}
// Could not find body method index, check regex
} else {
throw new Error(`Failed to inject patch: ${patch.class}`);
}
});
const patchSwf = _exec('java', instructions);
patchSwf.on('close', (code, signal) => {
if (code === 0) {
downloadAssets(clientProtocol, clientVersion, () => {
callback(clientProtocol, clientVersion);
});
}
});
});
}
});
});
}
});
}
function decompileClient(swfName, callback) {
console.log('Decompiling flash client...')
rimraf.sync('./cache/decompiled/scripts');
const cmd = _exec('java', [
'-jar', './bin/ffdec.jar',
'-format', 'script:as',
'-config', 'showMethodBodyId=1,parallelSpeedUp=0',
'-export', 'script', './cache/decompiled/',
`./clients/${swfName}`
]);
cmd.stdout.on('data', chunk => {console.log(chunk.toString())});
cmd.stderr.on('data', chunk => {console.log(chunk.toString())});
cmd.on('close', (code, signal) => {
if (code === 0) {
cmd.kill();
callback();
}
});
}
function commitDecompiledClient(swfName, protocol, version, callback) {
console.log('Pushing decompiled flash client...')
const commitTitle = `${protocol}.${version}`;
const repoPath = path.resolve(__dirname, './cache/decompiled');
const signature = git.Signature.now(
'Flash Updater',
'flash-updater@example.com'
);
let repo;
let index;
let oid;
git.Repository.open(repoPath)
.then(result => repo = result)
.then(() => repo.fetchAll({
certificateCheck: () => 1,
credentials: () => git.Cred.userpassPlaintextNew(GIT_TOKEN, 'x-oauth-basic')
}))
.then(() => repo.openIndex())
.then(result => {
index = result;
return index.read(1);
})
.then(() => index.addAll())
.then(() => index.write())
.then(() => index.writeTree())
.then(result => {
oid = result;
return git.Reference.nameToId(repo, 'HEAD');
})
.then(head => repo.getCommit(head))
.then(parent => repo.createCommit(
'HEAD', signature, signature, commitTitle, oid, [parent]
))
.then(() => repo.getRemote('origin'))
.then((remote) => remote.push(
['refs/heads/master:refs/heads/master'], {
callbacks: {
certificateCheck: () => 1,
credentials: () => git.Cred.userpassPlaintextNew(GIT_TOKEN, 'x-oauth-basic')
}
}
))
.done(() => {
console.log('Done. Update pushed to Github.');
swfFiles.push(`./clients/${swfName}`);
callback();
});
}
function addClient(swfName, protocol, version) {
decompileClient(swfName, () => {
commitDecompiledClient(swfName, protocol, version, () => console.log('Done.'));
});
}
startClientUpdater();
module.exports = [
// Replace CipSoft RSA with Open Tibia RSA
{
class: 'shared.cryptography.RSAPublicKey',
path: './cache/patches/scripts/shared/cryptography/RSAPublicKey.as',
reference: './cache/patches/scripts/shared/cryptography/RSAPublicKey.pcode',
pcode: 'RSAPublicKeyModulus.pcode',
regex: /PUBLIC_MODULUS:String\s?=.*method body index:\s?(\d+)/,
pcodeRegex: /findproperty Qname\(PrivateNamespace\(null,"(\d+)"\),"PUBLIC_EXPONENT"\)/
},
// Remove Focus Notifier
{
class: 'tibia.game.FocusNotifier',
path: './cache/patches/scripts/tibia/game/FocusNotifier.as',
reference: './cache/patches/scripts/tibia/game/FocusNotifier.as',
pcode: 'RemoveFocusNotifier.pcode',
regex: /public function show\(\) : void[^]*?\/\/ method body index: (\d+)/
}
];
trait method Qname(PackageNamespace(""),"show") dispid 0
method
name null
returns Qname(PackageNamespace(""),"void")
body
maxstack 3
localcount 2
initscopedepth 11
maxscopedepth 12
code
returnvoid
method
name null
returns null
body
maxstack 3
localcount 1
initscopedepth 3
maxscopedepth 4
code
getlocal_0
pushscope
findproperty Qname(PackageNamespace(""),"BLOCKSIZE")
pushshort 128
initproperty Qname(PackageNamespace(""),"BLOCKSIZE")
findproperty Qname(PrivateNamespace(null,"{{1}}"),"PUBLIC_EXPONENT")
pushint 65537
initproperty Qname(PrivateNamespace(null,"{{1}}"),"PUBLIC_EXPONENT")
findproperty Qname(PrivateNamespace(null,"{{1}}"),"PUBLIC_MODULUS")
pushstring "1091201329673994292788609605089955415282375029027981291234687579"
pushstring "3726629149257644633073969600111060390723088861007265581882535850"
add
pushstring "3429057592827629436413108566029093628212635953836686562675849720"
add
pushstring "6207862794310902180176810615217550567108238764764442605581471797"
add
pushstring "07119674283982419152118103759076030616683978566631413"
add
initproperty Qname(PrivateNamespace(null,"{{1}}"),"PUBLIC_MODULUS")
returnvoid
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment