Added forge checksum validations.

This commit is contained in:
Daniel Scalzi 2017-05-16 02:12:28 -04:00
parent 1a403f34a3
commit 51e75a610d
3 changed files with 104 additions and 11 deletions

View File

@ -28,6 +28,7 @@ const path = require('path')
const mkpath = require('mkdirp'); const mkpath = require('mkdirp');
const async = require('async') const async = require('async')
const crypto = require('crypto') const crypto = require('crypto')
const AdmZip = require('adm-zip')
const EventEmitter = require('events'); const EventEmitter = require('events');
const {remote} = require('electron') const {remote} = require('electron')
@ -155,6 +156,37 @@ const instance = new AssetGuard()
// Utility Functions // Utility Functions
/**
* Calculates the hash for a file using the specified algorithm.
*
* @param {Buffer} buf - the buffer containing file data.
* @param {String} algo - the hash algorithm.
* @returns {String} - the calculated hash in hex.
*/
function _calculateHash(buf, algo){
return crypto.createHash(algo).update(buf).digest('hex')
}
/**
* Used to parse a checksums file. This is specifically designed for
* the checksums.sha1 files found inside the forge scala dependencies.
*
* @param {String} content - the string content of the checksums file.
* @returns {Object} - an object with keys being the file names, and values being the hashes.
*/
function _parseChecksumsFile(content){
let finalContent = {}
let lines = content.split('\n')
for(let i=0; i<lines.length; i++){
let bits = lines[i].split(' ')
if(bits[1] == null) {
continue
}
finalContent[bits[1]] = bits[0]
}
return finalContent
}
/** /**
* Validate that a file exists and matches a given hash value. * Validate that a file exists and matches a given hash value.
* *
@ -163,18 +195,79 @@ const instance = new AssetGuard()
* @param {String} hash - the existing hash to check against. * @param {String} hash - the existing hash to check against.
* @returns {Boolean} - true if the file exists and calculated hash matches the given hash, otherwise false. * @returns {Boolean} - true if the file exists and calculated hash matches the given hash, otherwise false.
*/ */
function validateLocal(filePath, algo, hash){ function _validateLocal(filePath, algo, hash){
if(fs.existsSync(filePath)){ if(fs.existsSync(filePath)){
let fileName = path.basename(filePath) let fileName = path.basename(filePath)
let shasum = crypto.createHash(algo) let buf = fs.readFileSync(filePath)
let content = fs.readFileSync(filePath) let calcdhash = _calculateHash(buf, algo)
shasum.update(content)
let calcdhash = shasum.digest('hex')
return calcdhash === hash return calcdhash === hash
} }
return false; return false;
} }
/**
* Validates a file in the style used by forge's version index.
*
* @param {String} filePath - the path of the file to validate.
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
* @returns {Boolean} - true if the file exists and the hashes match, otherwise false.
*/
function _validateForgeChecksum(filePath, checksums){
if(fs.existsSync(filePath)){
if(checksums == null || checksums.length === 0){
return true
}
let buf = fs.readFileSync(filePath)
let calcdhash = _calculateHash(buf, 'sha1')
let valid = checksums.includes(calcdhash)
if(!valid && filePath.endsWith('.jar')){
valid = _validateForgeJar(filePath, checksums)
}
return valid
}
return false
}
/**
* Validates a forge jar file dependency who declares a checksums.sha1 file.
* This can be an expensive task as it usually requires that we calculate thousands
* of hashes.
*
* @param {Buffer} buf - the buffer of the jar file.
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
* @returns {Boolean} - true if all hashes declared in the checksums.sha1 file match the actual hashes.
*/
function _validateForgeJar(buf, checksums){
const hashes = {}
let expected = {}
const zip = new AdmZip(buf)
const zipEntries = zip.getEntries()
//First pass
for(let i=0; i<zipEntries.length; i++){
let entry = zipEntries[i]
if(entry.entryName === 'checksums.sha1'){
expected = _parseChecksumsFile(zip.readAsText(entry))
}
hashes[entry.entryName] = _calculateHash(entry.getData(), 'sha1')
}
if(!checksums.includes(hashes['checksums.sha1'])){
return false
}
//Check against expected
const expectedEntries = Object.keys(expected)
for(let i=0; i<expectedEntries.length; i++){
if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
return false
}
}
return true
}
/** /**
* Initiate an async download process for an AssetGuard DLTracker. * Initiate an async download process for an AssetGuard DLTracker.
* *
@ -335,7 +428,7 @@ function _assetChainValidateAssets(versionData, basePath, indexData){
const assetName = path.join(hash.substring(0, 2), hash) const assetName = path.join(hash.substring(0, 2), hash)
const urlName = hash.substring(0, 2) + "/" + hash const urlName = hash.substring(0, 2) + "/" + hash
const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName)) const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
if(!validateLocal(ast.to, 'sha1', ast.hash)){ if(!_validateLocal(ast.to, 'sha1', ast.hash)){
dlSize += (ast.size*1) dlSize += (ast.size*1)
assetDlQueue.push(ast) assetDlQueue.push(ast)
} }
@ -372,7 +465,7 @@ function validateLibraries(versionData, basePath){
if(Library.validateRules(lib.rules)){ if(Library.validateRules(lib.rules)){
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]] let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
if(!validateLocal(libItm.to, 'sha1', libItm.hash)){ if(!_validateLocal(libItm.to, 'sha1', libItm.hash)){
dlSize += (libItm.size*1) dlSize += (libItm.size*1)
libDlQueue.push(libItm) libDlQueue.push(libItm)
} }
@ -419,7 +512,7 @@ function validateClient(versionData, basePath, force = false){
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile)) let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
if(!validateLocal(client.to, 'sha1', client.hash) || force){ if(!_validateLocal(client.to, 'sha1', client.hash) || force){
instance.files.dlqueue.push(client) instance.files.dlqueue.push(client)
instance.files.dlsize += client.size*1 instance.files.dlsize += client.size*1
fulfill() fulfill()
@ -445,7 +538,7 @@ function validateLogConfig(versionData, basePath){
let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id)) let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
if(!validateLocal(logConfig.to, 'sha1', logConfig.hash)){ if(!_validateLocal(logConfig.to, 'sha1', logConfig.hash)){
instance.files.dlqueue.push(logConfig) instance.files.dlqueue.push(logConfig)
instance.files.dlsize += client.size*1 instance.files.dlsize += client.size*1
fulfill() fulfill()

View File

@ -4,9 +4,9 @@ const path = require('path')
const child_process = require('child_process') const child_process = require('child_process')
const ag = require('./assetguard.js') const ag = require('./assetguard.js')
const fs = require('fs') const fs = require('fs')
const unzip = require('unzip')
const mkpath = require('mkdirp'); const mkpath = require('mkdirp');
/* TODO - convert native extraction to use adm-zip. Currently not functional due to removal of unzip module (it was bad) */
launchMinecraft = function(versionData, basePath){ launchMinecraft = function(versionData, basePath){
const authPromise = mojang.auth('EMAIL', 'PASS', uuidV4(), { const authPromise = mojang.auth('EMAIL', 'PASS', uuidV4(), {
name: 'Minecraft', name: 'Minecraft',

View File

@ -17,13 +17,13 @@
}, },
"homepage": "http://www.westeroscraft.com/", "homepage": "http://www.westeroscraft.com/",
"dependencies": { "dependencies": {
"adm-zip": "^0.4.7",
"async": "^2.3.0", "async": "^2.3.0",
"electron": "^1.6.5", "electron": "^1.6.5",
"extract-zip": "=1.6.0", "extract-zip": "=1.6.0",
"jQuery": "^1.7.4", "jQuery": "^1.7.4",
"mojang": "^0.4.1", "mojang": "^0.4.1",
"promise": "^7.1.1", "promise": "^7.1.1",
"unzip": "^0.1.11",
"uuid": "^3.0.1" "uuid": "^3.0.1"
} }
} }