Last active
March 30, 2020 07:40
-
-
Save TheUltDev/d29e5741d6057f5de03378a7e13cfab1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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+)/ | |
} | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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