diff --git a/app/assets/js/assetexec.js b/app/assets/js/assetexec.js index 9d39449d..8e32b924 100644 --- a/app/assets/js/assetexec.js +++ b/app/assets/js/assetexec.js @@ -1,73 +1,73 @@ -let target = require('./assetguard')[process.argv[2]] -if(target == null){ - process.send({context: 'error', data: null, error: 'Invalid class name'}) - console.error('Invalid class name passed to argv[2], cannot continue.') - process.exit(1) -} -let tracker = new target(...(process.argv.splice(3))) - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' - -//const tracker = new AssetGuard(process.argv[2], process.argv[3]) -console.log('AssetExec Started') - -// Temporary for debug purposes. -process.on('unhandledRejection', r => console.log(r)) - -let percent = 0 -function assignListeners(){ - tracker.on('validate', (data) => { - process.send({context: 'validate', data}) - }) - tracker.on('progress', (data, acc, total) => { - const currPercent = parseInt((acc/total) * 100) - if (currPercent !== percent) { - percent = currPercent - process.send({context: 'progress', data, value: acc, total, percent}) - } - }) - tracker.on('complete', (data, ...args) => { - process.send({context: 'complete', data, args}) - }) - tracker.on('error', (data, error) => { - process.send({context: 'error', data, error}) - }) -} - -assignListeners() - -process.on('message', (msg) => { - if(msg.task === 'execute'){ - const func = msg.function - let nS = tracker[func] // Nonstatic context - let iS = target[func] // Static context - if(typeof nS === 'function' || typeof iS === 'function'){ - const f = typeof nS === 'function' ? nS : iS - const res = f.apply(f === nS ? tracker : null, msg.argsArr) - if(res instanceof Promise){ - res.then((v) => { - process.send({result: v, context: func}) - }).catch((err) => { - process.send({result: err.message || err, context: func}) - }) - } else { - process.send({result: res, context: func}) - } - } else { - process.send({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`}) - } - } else if(msg.task === 'changeContext'){ - target = require('./assetguard')[msg.class] - if(target == null){ - process.send({context: 'error', data: null, error: `Invalid class ${msg.class}`}) - } else { - tracker = new target(...(msg.args)) - assignListeners() - } - } -}) - -process.on('disconnect', () => { - console.log('AssetExec Disconnected') - process.exit(0) +let target = require('./assetguard')[process.argv[2]] +if(target == null){ + process.send({context: 'error', data: null, error: 'Invalid class name'}) + console.error('Invalid class name passed to argv[2], cannot continue.') + process.exit(1) +} +let tracker = new target(...(process.argv.splice(3))) + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + +//const tracker = new AssetGuard(process.argv[2], process.argv[3]) +console.log('AssetExec Started') + +// Temporary for debug purposes. +process.on('unhandledRejection', r => console.log(r)) + +let percent = 0 +function assignListeners(){ + tracker.on('validate', (data) => { + process.send({context: 'validate', data}) + }) + tracker.on('progress', (data, acc, total) => { + const currPercent = parseInt((acc/total) * 100) + if (currPercent !== percent) { + percent = currPercent + process.send({context: 'progress', data, value: acc, total, percent}) + } + }) + tracker.on('complete', (data, ...args) => { + process.send({context: 'complete', data, args}) + }) + tracker.on('error', (data, error) => { + process.send({context: 'error', data, error}) + }) +} + +assignListeners() + +process.on('message', (msg) => { + if(msg.task === 'execute'){ + const func = msg.function + let nS = tracker[func] // Nonstatic context + let iS = target[func] // Static context + if(typeof nS === 'function' || typeof iS === 'function'){ + const f = typeof nS === 'function' ? nS : iS + const res = f.apply(f === nS ? tracker : null, msg.argsArr) + if(res instanceof Promise){ + res.then((v) => { + process.send({result: v, context: func}) + }).catch((err) => { + process.send({result: err.message || err, context: func}) + }) + } else { + process.send({result: res, context: func}) + } + } else { + process.send({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`}) + } + } else if(msg.task === 'changeContext'){ + target = require('./assetguard')[msg.class] + if(target == null){ + process.send({context: 'error', data: null, error: `Invalid class ${msg.class}`}) + } else { + tracker = new target(...(msg.args)) + assignListeners() + } + } +}) + +process.on('disconnect', () => { + console.log('AssetExec Disconnected') + process.exit(0) }) \ No newline at end of file diff --git a/app/assets/js/assetguard.js b/app/assets/js/assetguard.js index 7612bffb..31fb4c20 100644 --- a/app/assets/js/assetguard.js +++ b/app/assets/js/assetguard.js @@ -1,1915 +1,1915 @@ -// Requirements -const AdmZip = require('adm-zip') -const async = require('async') -const child_process = require('child_process') -const crypto = require('crypto') -const EventEmitter = require('events') -const fs = require('fs-extra') -const path = require('path') -const Registry = require('winreg') -const request = require('request') -const tar = require('tar-fs') -const zlib = require('zlib') - -const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') -const isDev = require('./isdev') - -// Constants -// const PLATFORM_MAP = { -// win32: '-windows-x64.tar.gz', -// darwin: '-macosx-x64.tar.gz', -// linux: '-linux-x64.tar.gz' -// } - -// Classes - -/** Class representing a base asset. */ -class Asset { - /** - * Create an asset. - * - * @param {any} id The id of the asset. - * @param {string} hash The hash value of the asset. - * @param {number} size The size in bytes of the asset. - * @param {string} from The url where the asset can be found. - * @param {string} to The absolute local file path of the asset. - */ - constructor(id, hash, size, from, to){ - this.id = id - this.hash = hash - this.size = size - this.from = from - this.to = to - } -} - -/** Class representing a mojang library. */ -class Library extends Asset { - - /** - * Converts the process.platform OS names to match mojang's OS names. - */ - static mojangFriendlyOS(){ - const opSys = process.platform - if (opSys === 'darwin') { - return 'osx' - } else if (opSys === 'win32'){ - return 'windows' - } else if (opSys === 'linux'){ - return 'linux' - } else { - return 'unknown_os' - } - } - - /** - * Checks whether or not a library is valid for download on a particular OS, following - * the rule format specified in the mojang version data index. If the allow property has - * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow - * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING - * the one specified. - * - * If the rules are undefined, the natives property will be checked for a matching entry - * for the current OS. - * - * @param {Array.} rules The Library's download rules. - * @param {Object} natives The Library's natives object. - * @returns {boolean} True if the Library follows the specified rules, otherwise false. - */ - static validateRules(rules, natives){ - if(rules == null) { - if(natives == null) { - return true - } else { - return natives[Library.mojangFriendlyOS()] != null - } - } - - for(let rule of rules){ - const action = rule.action - const osProp = rule.os - if(action != null && osProp != null){ - const osName = osProp.name - const osMoj = Library.mojangFriendlyOS() - if(action === 'allow'){ - return osName === osMoj - } else if(action === 'disallow'){ - return osName !== osMoj - } - } - } - return true - } -} - -class DistroModule extends Asset { - - /** - * Create a DistroModule. This is for processing, - * not equivalent to the module objects in the - * distro index. - * - * @param {any} id The id of the asset. - * @param {string} hash The hash value of the asset. - * @param {number} size The size in bytes of the asset. - * @param {string} from The url where the asset can be found. - * @param {string} to The absolute local file path of the asset. - * @param {string} type The the module type. - */ - constructor(id, hash, size, from, to, type){ - super(id, hash, size, from, to) - this.type = type - } - -} - -/** - * Class representing a download tracker. This is used to store meta data - * about a download queue, including the queue itself. - */ -class DLTracker { - - /** - * Create a DLTracker - * - * @param {Array.} dlqueue An array containing assets queued for download. - * @param {number} dlsize The combined size of each asset in the download queue array. - * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading. - */ - constructor(dlqueue, dlsize, callback = null){ - this.dlqueue = dlqueue - this.dlsize = dlsize - this.callback = callback - } - -} - -class Util { - - /** - * Returns true if the actual version is greater than - * or equal to the desired version. - * - * @param {string} desired The desired version. - * @param {string} actual The actual version. - */ - static mcVersionAtLeast(desired, actual){ - const des = desired.split('.') - const act = actual.split('.') - - for(let i=0; i= parseInt(des[i]))){ - return false - } - } - return true - } - - static isForgeGradle3(mcVersion, forgeVersion) { - - if(Util.mcVersionAtLeast('1.13', mcVersion)) { - return true - } - - try { - - const forgeVer = forgeVersion.split('-')[1] - - const maxFG2 = [14, 23, 5, 2847] - const verSplit = forgeVer.split('.').map(v => Number(v)) - - for(let i=0; i maxFG2[i]) { - return true - } else if(verSplit[i] < maxFG2[i]) { - return false - } - } - - return false - - } catch(err) { - throw new Error('Forge version is complex (changed).. launcher requires a patch.') - } - } - - static isAutoconnectBroken(forgeVersion) { - - const forgeVer = forgeVersion.split('-')[1] - - const minWorking = [31, 2, 15] - const verSplit = forgeVer.split('.').map(v => Number(v)) - - if(verSplit[0] === 31) { - for(let i=0; i minWorking[i]) { - return false - } else if(verSplit[i] < minWorking[i]) { - return true - } - } - } - - return false - } - -} - - -class JavaGuard extends EventEmitter { - - constructor(mcVersion){ - super() - this.mcVersion = mcVersion - } - - // /** - // * @typedef OracleJREData - // * @property {string} uri The base uri of the JRE. - // * @property {{major: string, update: string, build: string}} version Object containing version information. - // */ - - // /** - // * Resolves the latest version of Oracle's JRE and parses its download link. - // * - // * @returns {Promise.} Promise which resolved to an object containing the JRE download data. - // */ - // static _latestJREOracle(){ - - // const url = 'https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html' - // const regex = /https:\/\/.+?(?=\/java)\/java\/jdk\/([0-9]+u[0-9]+)-(b[0-9]+)\/([a-f0-9]{32})?\/jre-\1/ - - // return new Promise((resolve, reject) => { - // request(url, (err, resp, body) => { - // if(!err){ - // const arr = body.match(regex) - // const verSplit = arr[1].split('u') - // resolve({ - // uri: arr[0], - // version: { - // major: verSplit[0], - // update: verSplit[1], - // build: arr[2] - // } - // }) - // } else { - // resolve(null) - // } - // }) - // }) - // } - - /** - * @typedef OpenJDKData - * @property {string} uri The base uri of the JRE. - * @property {number} size The size of the download. - * @property {string} name The name of the artifact. - */ - - /** - * Fetch the last open JDK binary. Uses https://api.adoptopenjdk.net/ - * - * @param {string} major The major version of Java to fetch. - * - * @returns {Promise.} Promise which resolved to an object containing the JRE download data. - */ - static _latestOpenJDK(major = '8'){ - - const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform) - - const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre` - - return new Promise((resolve, reject) => { - request({url, json: true}, (err, resp, body) => { - if(!err && body.length > 0){ - resolve({ - uri: body[0].binary_link, - size: body[0].binary_size, - name: body[0].binary_name - }) - } else { - resolve(null) - } - }) - }) - } - - /** - * Returns the path of the OS-specific executable for the given Java - * installation. Supported OS's are win32, darwin, linux. - * - * @param {string} rootDir The root directory of the Java installation. - * @returns {string} The path to the Java executable. - */ - static javaExecFromRoot(rootDir){ - if(process.platform === 'win32'){ - return path.join(rootDir, 'bin', 'javaw.exe') - } else if(process.platform === 'darwin'){ - return path.join(rootDir, 'Contents', 'Home', 'bin', 'java') - } else if(process.platform === 'linux'){ - return path.join(rootDir, 'bin', 'java') - } - return rootDir - } - - /** - * Check to see if the given path points to a Java executable. - * - * @param {string} pth The path to check against. - * @returns {boolean} True if the path points to a Java executable, otherwise false. - */ - static isJavaExecPath(pth){ - if(process.platform === 'win32'){ - return pth.endsWith(path.join('bin', 'javaw.exe')) - } else if(process.platform === 'darwin'){ - return pth.endsWith(path.join('bin', 'java')) - } else if(process.platform === 'linux'){ - return pth.endsWith(path.join('bin', 'java')) - } - return false - } - - /** - * Load Mojang's launcher.json file. - * - * @returns {Promise.} Promise which resolves to Mojang's launcher.json object. - */ - static loadMojangLauncherData(){ - return new Promise((resolve, reject) => { - request.get('https://launchermeta.mojang.com/mc/launcher.json', (err, resp, body) => { - if(err){ - resolve(null) - } else { - resolve(JSON.parse(body)) - } - }) - }) - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Dynamically detects the formatting - * to use. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static parseJavaRuntimeVersion(verString){ - const major = verString.split('.')[0] - if(major == 1){ - return JavaGuard._parseJavaRuntimeVersion_8(verString) - } else { - return JavaGuard._parseJavaRuntimeVersion_9(verString) - } - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Uses Java 8 formatting. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static _parseJavaRuntimeVersion_8(verString){ - // 1.{major}.0_{update}-b{build} - // ex. 1.8.0_152-b16 - const ret = {} - let pts = verString.split('-') - ret.build = parseInt(pts[1].substring(1)) - pts = pts[0].split('_') - ret.update = parseInt(pts[1]) - ret.major = parseInt(pts[0].split('.')[1]) - return ret - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Uses Java 9+ formatting. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static _parseJavaRuntimeVersion_9(verString){ - // {major}.{minor}.{revision}+{build} - // ex. 10.0.2+13 - const ret = {} - let pts = verString.split('+') - ret.build = parseInt(pts[1]) - pts = pts[0].split('.') - ret.major = parseInt(pts[0]) - ret.minor = parseInt(pts[1]) - ret.revision = parseInt(pts[2]) - return ret - } - - /** - * Validates the output of a JVM's properties. Currently validates that a JRE is x64 - * and that the major = 8, update > 52. - * - * @param {string} stderr The output to validate. - * - * @returns {Promise.} A promise which resolves to a meta object about the JVM. - * The validity is stored inside the `valid` property. - */ - _validateJVMProperties(stderr){ - const res = stderr - const props = res.split('\n') - - const goal = 2 - let checksum = 0 - - const meta = {} - - for(let i=0; i -1){ - let arch = props[i].split('=')[1].trim() - arch = parseInt(arch) - console.log(props[i].trim()) - if(arch === 64){ - meta.arch = arch - ++checksum - if(checksum === goal){ - break - } - } - } else if(props[i].indexOf('java.runtime.version') > -1){ - let verString = props[i].split('=')[1].trim() - console.log(props[i].trim()) - const verOb = JavaGuard.parseJavaRuntimeVersion(verString) - if(verOb.major < 9){ - // Java 8 - if(verOb.major === 8 && verOb.update > 52){ - meta.version = verOb - ++checksum - if(checksum === goal){ - break - } - } - } else { - // Java 9+ - if(Util.mcVersionAtLeast('1.13', this.mcVersion)){ - console.log('Java 9+ not yet tested.') - /* meta.version = verOb - ++checksum - if(checksum === goal){ - break - } */ - } - } - } - } - - meta.valid = checksum === goal - - return meta - } - - /** - * Validates that a Java binary is at least 64 bit. This makes use of the non-standard - * command line option -XshowSettings:properties. The output of this contains a property, - * sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported - * in Java 8 and 9. Since this is a non-standard option. This will resolve to true if - * the function's code throws errors. That would indicate that the option is changed or - * removed. - * - * @param {string} binaryExecPath Path to the java executable we wish to validate. - * - * @returns {Promise.} A promise which resolves to a meta object about the JVM. - * The validity is stored inside the `valid` property. - */ - _validateJavaBinary(binaryExecPath){ - - return new Promise((resolve, reject) => { - if(!JavaGuard.isJavaExecPath(binaryExecPath)){ - resolve({valid: false}) - } else if(fs.existsSync(binaryExecPath)){ - // Workaround (javaw.exe no longer outputs this information.) - console.log(typeof binaryExecPath) - if(binaryExecPath.indexOf('javaw.exe') > -1) { - binaryExecPath.replace('javaw.exe', 'java.exe') - } - child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => { - try { - // Output is stored in stderr? - resolve(this._validateJVMProperties(stderr)) - } catch (err){ - // Output format might have changed, validation cannot be completed. - resolve({valid: false}) - } - }) - } else { - resolve({valid: false}) - } - }) - - } - - /** - * Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check - * to see if the value points to a path which exists. If the path exits, the path is returned. - * - * @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null. - */ - static _scanJavaHome(){ - const jHome = process.env.JAVA_HOME - try { - let res = fs.existsSync(jHome) - return res ? jHome : null - } catch (err) { - // Malformed JAVA_HOME property. - return null - } - } - - /** - * Scans the registry for 64-bit Java entries. The paths of each entry are added to - * a set and returned. Currently, only Java 8 (1.8) is supported. - * - * @returns {Promise.>} A promise which resolves to a set of 64-bit Java root - * paths found in the registry. - */ - static _scanRegistry(){ - - return new Promise((resolve, reject) => { - // Keys for Java v9.0.0 and later: - // 'SOFTWARE\\JavaSoft\\JRE' - // 'SOFTWARE\\JavaSoft\\JDK' - // Forge does not yet support Java 9, therefore we do not. - - // Keys for Java 1.8 and prior: - const regKeys = [ - '\\SOFTWARE\\JavaSoft\\Java Runtime Environment', - '\\SOFTWARE\\JavaSoft\\Java Development Kit' - ] - - let keysDone = 0 - - const candidates = new Set() - - for(let i=0; i { - if(exists) { - key.keys((err, javaVers) => { - if(err){ - keysDone++ - console.error(err) - - // REG KEY DONE - // DUE TO ERROR - if(keysDone === regKeys.length){ - resolve(candidates) - } - } else { - if(javaVers.length === 0){ - // REG KEY DONE - // NO SUBKEYS - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } else { - - let numDone = 0 - - for(let j=0; j { - const jHome = res.value - if(jHome.indexOf('(x86)') === -1){ - candidates.add(jHome) - } - - // SUBKEY DONE - - numDone++ - if(numDone === javaVers.length){ - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - }) - } else { - - // SUBKEY DONE - // NOT JAVA 8 - - numDone++ - if(numDone === javaVers.length){ - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - } - } - } - } - }) - } else { - - // REG KEY DONE - // DUE TO NON-EXISTANCE - - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - }) - } - - }) - - } - - /** - * See if JRE exists in the Internet Plug-Ins folder. - * - * @returns {string} The path of the JRE if found, otherwise null. - */ - static _scanInternetPlugins(){ - // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java - const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin' - const res = fs.existsSync(JavaGuard.javaExecFromRoot(pth)) - return res ? pth : null - } - - /** - * Scan a directory for root JVM folders. - * - * @param {string} scanDir The directory to scan. - * @returns {Promise.>} A promise which resolves to a set of the discovered - * root JVM folders. - */ - static _scanFileSystem(scanDir){ - return new Promise((resolve, reject) => { - - fs.exists(scanDir, (e) => { - - let res = new Set() - - if(e){ - fs.readdir(scanDir, (err, files) => { - if(err){ - resolve(res) - console.log(err) - } else { - let pathsDone = 0 - - for(let i=0; i { - - if(v){ - res.add(combinedPath) - } - - ++pathsDone - - if(pathsDone === files.length){ - resolve(res) - } - - }) - } - if(pathsDone === files.length){ - resolve(res) - } - } - }) - } else { - resolve(res) - } - }) - - }) - } - - /** - * - * @param {Set.} rootSet A set of JVM root strings to validate. - * @returns {Promise.} A promise which resolves to an array of meta objects - * for each valid JVM root directory. - */ - async _validateJavaRootSet(rootSet){ - - const rootArr = Array.from(rootSet) - const validArr = [] - - for(let i=0; i { - - if(a.version.major === b.version.major){ - - if(a.version.major < 9){ - // Java 8 - if(a.version.update === b.version.update){ - if(a.version.build === b.version.build){ - - // Same version, give priority to JRE. - if(a.execPath.toLowerCase().indexOf('jdk') > -1){ - return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 - } else { - return -1 - } - - } else { - return a.version.build > b.version.build ? -1 : 1 - } - } else { - return a.version.update > b.version.update ? -1 : 1 - } - } else { - // Java 9+ - if(a.version.minor === b.version.minor){ - if(a.version.revision === b.version.revision){ - - // Same version, give priority to JRE. - if(a.execPath.toLowerCase().indexOf('jdk') > -1){ - return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 - } else { - return -1 - } - - } else { - return a.version.revision > b.version.revision ? -1 : 1 - } - } else { - return a.version.minor > b.version.minor ? -1 : 1 - } - } - - } else { - return a.version.major > b.version.major ? -1 : 1 - } - }) - - return retArr - } - - /** - * Attempts to find a valid x64 installation of Java on Windows machines. - * Possible paths will be pulled from the registry and the JAVA_HOME environment - * variable. The paths will be sorted with higher versions preceeding lower, and - * JREs preceeding JDKs. The binaries at the sorted paths will then be validated. - * The first validated is returned. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _win32JavaValidate(dataDir){ - - // Get possible paths from the registry. - let pathSet1 = await JavaGuard._scanRegistry() - if(pathSet1.length === 0){ - // Do a manual file system scan of program files. - pathSet1 = JavaGuard._scanFileSystem('C:\\Program Files\\Java') - } - - // Get possible paths from the data directory. - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - // Merge the results. - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Validate JAVA_HOME. - const jHome = JavaGuard._scanJavaHome() - if(jHome != null && jHome.indexOf('(x86)') === -1){ - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - - } - - /** - * Attempts to find a valid x64 installation of Java on MacOS. - * The system JVM directory is scanned for possible installations. - * The JAVA_HOME enviroment variable and internet plugins directory - * are also scanned and validated. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _darwinJavaValidate(dataDir){ - - const pathSet1 = await JavaGuard._scanFileSystem('/Library/Java/JavaVirtualMachines') - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Check Internet Plugins folder. - const iPPath = JavaGuard._scanInternetPlugins() - if(iPPath != null){ - uberSet.add(iPPath) - } - - // Check the JAVA_HOME environment variable. - let jHome = JavaGuard._scanJavaHome() - if(jHome != null){ - // Ensure we are at the absolute root. - if(jHome.contains('/Contents/Home')){ - jHome = jHome.substring(0, jHome.indexOf('/Contents/Home')) - } - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - } - - /** - * Attempts to find a valid x64 installation of Java on Linux. - * The system JVM directory is scanned for possible installations. - * The JAVA_HOME enviroment variable is also scanned and validated. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _linuxJavaValidate(dataDir){ - - const pathSet1 = await JavaGuard._scanFileSystem('/usr/lib/jvm') - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Validate JAVA_HOME - const jHome = JavaGuard._scanJavaHome() - if(jHome != null){ - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - } - - /** - * Retrieve the path of a valid x64 Java installation. - * - * @param {string} dataDir The base launcher directory. - * @returns {string} A path to a valid x64 Java installation, null if none found. - */ - async validateJava(dataDir){ - return await this['_' + process.platform + 'JavaValidate'](dataDir) - } - -} - - - - -/** - * Central object class used for control flow. This object stores data about - * categories of downloads. Each category is assigned an identifier with a - * DLTracker object as its value. Combined information is also stored, such as - * the total size of all the queued files in each category. This event is used - * to emit events so that external modules can listen into processing done in - * this module. - */ -class AssetGuard extends EventEmitter { - - /** - * Create an instance of AssetGuard. - * On creation the object's properties are never-null default - * values. Each identifier is resolved to an empty DLTracker. - * - * @param {string} commonPath The common path for shared game files. - * @param {string} javaexec The path to a java executable which will be used - * to finalize installation. - */ - constructor(commonPath, javaexec){ - super() - this.totaldlsize = 0 - this.progress = 0 - this.assets = new DLTracker([], 0) - this.libraries = new DLTracker([], 0) - this.files = new DLTracker([], 0) - this.forge = new DLTracker([], 0) - this.java = new DLTracker([], 0) - this.extractQueue = [] - this.commonPath = commonPath - this.javaexec = javaexec - } - - // Static Utility Functions - // #region - - // Static Hash Validation Functions - // #region - - /** - * 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. - */ - static _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. - */ - static _parseChecksumsFile(content){ - let finalContent = {} - let lines = content.split('\n') - for(let i=0; i} checksums The checksums listed in the forge version index. - * @returns {boolean} True if the file exists and the hashes match, otherwise false. - */ - static _validateForgeChecksum(filePath, checksums){ - if(fs.existsSync(filePath)){ - if(checksums == null || checksums.length === 0){ - return true - } - let buf = fs.readFileSync(filePath) - let calcdhash = AssetGuard._calculateHash(buf, 'sha1') - let valid = checksums.includes(calcdhash) - if(!valid && filePath.endsWith('.jar')){ - valid = AssetGuard._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.} 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. - */ - static _validateForgeJar(buf, checksums){ - // Double pass method was the quickest I found. I tried a version where we store data - // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time. - - const hashes = {} - let expected = {} - - const zip = new AdmZip(buf) - const zipEntries = zip.getEntries() - - //First pass - for(let i=0; i} filePaths The paths of the files to be extracted and unpacked. - * @returns {Promise.} An empty promise to indicate the extraction has completed. - */ - static _extractPackXZ(filePaths, javaExecutable){ - console.log('[PackXZExtract] Starting') - return new Promise((resolve, reject) => { - - let libPath - if(isDev){ - libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') - } else { - if(process.platform === 'darwin'){ - libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar') - } else { - libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar') - } - } - - const filePath = filePaths.join(',') - const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath]) - child.stdout.on('data', (data) => { - console.log('[PackXZExtract]', data.toString('utf8')) - }) - child.stderr.on('data', (data) => { - console.log('[PackXZExtract]', data.toString('utf8')) - }) - child.on('close', (code, signal) => { - console.log('[PackXZExtract]', 'Exited with code', code) - resolve() - }) - }) - } - - /** - * Function which finalizes the forge installation process. This creates a 'version' - * instance for forge and saves its version.json file into that instance. If that - * instance already exists, the contents of the version.json file are read and returned - * in a promise. - * - * @param {Asset} asset The Asset object representing Forge. - * @param {string} commonPath The common path for shared game files. - * @returns {Promise.} A promise which resolves to the contents of forge's version.json. - */ - static _finalizeForgeAsset(asset, commonPath){ - return new Promise((resolve, reject) => { - fs.readFile(asset.to, (err, data) => { - const zip = new AdmZip(data) - const zipEntries = zip.getEntries() - - for(let i=0; i} Promise which resolves to the version data object. - */ - loadVersionData(version, force = false){ - const self = this - return new Promise(async (resolve, reject) => { - const versionPath = path.join(self.commonPath, 'versions', version) - const versionFile = path.join(versionPath, version + '.json') - if(!fs.existsSync(versionFile) || force){ - const url = await self._getVersionDataUrl(version) - //This download will never be tracked as it's essential and trivial. - console.log('Preparing download of ' + version + ' assets.') - fs.ensureDirSync(versionPath) - const stream = request(url).pipe(fs.createWriteStream(versionFile)) - stream.on('finish', () => { - resolve(JSON.parse(fs.readFileSync(versionFile))) - }) - } else { - resolve(JSON.parse(fs.readFileSync(versionFile))) - } - }) - } - - /** - * Parses Mojang's version manifest and retrieves the url of the version - * data index. - * - * @param {string} version The version to lookup. - * @returns {Promise.} Promise which resolves to the url of the version data index. - * If the version could not be found, resolves to null. - */ - _getVersionDataUrl(version){ - return new Promise((resolve, reject) => { - request('https://launchermeta.mojang.com/mc/game/version_manifest.json', (error, resp, body) => { - if(error){ - reject(error) - } else { - const manifest = JSON.parse(body) - - for(let v of manifest.versions){ - if(v.id === version){ - resolve(v.url) - } - } - - resolve(null) - } - }) - }) - } - - - // Asset (Category=''') Validation Functions - // #region - - /** - * Public asset validation function. This function will handle the validation of assets. - * It will parse the asset index specified in the version data, analyzing each - * asset entry. In this analysis it will check to see if the local file exists and is valid. - * If not, it will be added to the download queue for the 'assets' identifier. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateAssets(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - self._assetChainIndexData(versionData, force).then(() => { - resolve() - }) - }) - } - - //Chain the asset tasks to provide full async. The below functions are private. - /** - * Private function used to chain the asset validation process. This function retrieves - * the index data. - * @param {Object} versionData - * @param {boolean} force - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - _assetChainIndexData(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - //Asset index constants. - const assetIndex = versionData.assetIndex - const name = assetIndex.id + '.json' - const indexPath = path.join(self.commonPath, 'assets', 'indexes') - const assetIndexLoc = path.join(indexPath, name) - - let data = null - if(!fs.existsSync(assetIndexLoc) || force){ - console.log('Downloading ' + versionData.id + ' asset index.') - fs.ensureDirSync(indexPath) - const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc)) - stream.on('finish', () => { - data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) - self._assetChainValidateAssets(versionData, data).then(() => { - resolve() - }) - }) - } else { - data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) - self._assetChainValidateAssets(versionData, data).then(() => { - resolve() - }) - } - }) - } - - /** - * Private function used to chain the asset validation process. This function processes - * the assets and enqueues missing or invalid files. - * @param {Object} versionData - * @param {boolean} force - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - _assetChainValidateAssets(versionData, indexData){ - const self = this - return new Promise((resolve, reject) => { - - //Asset constants - const resourceURL = 'http://resources.download.minecraft.net/' - const localPath = path.join(self.commonPath, 'assets') - const objectPath = path.join(localPath, 'objects') - - const assetDlQueue = [] - let dlSize = 0 - let acc = 0 - const total = Object.keys(indexData.objects).length - //const objKeys = Object.keys(data.objects) - async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => { - acc++ - self.emit('progress', 'assets', acc, total) - const hash = value.hash - const assetName = path.join(hash.substring(0, 2), hash) - const urlName = hash.substring(0, 2) + '/' + hash - const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName)) - if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){ - dlSize += (ast.size*1) - assetDlQueue.push(ast) - } - cb() - }, (err) => { - self.assets = new DLTracker(assetDlQueue, dlSize) - resolve() - }) - }) - } - - // #endregion - - // Library (Category=''') Validation Functions - // #region - - /** - * Public library validation function. This function will handle the validation of libraries. - * It will parse the version data, analyzing each library entry. In this analysis, it will - * check to see if the local file exists and is valid. If not, it will be added to the download - * queue for the 'libraries' identifier. - * - * @param {Object} versionData The version data for the assets. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateLibraries(versionData){ - const self = this - return new Promise((resolve, reject) => { - - const libArr = versionData.libraries - const libPath = path.join(self.commonPath, 'libraries') - - const libDlQueue = [] - let dlSize = 0 - - //Check validity of each library. If the hashs don't match, download the library. - async.eachLimit(libArr, 5, (lib, cb) => { - if(Library.validateRules(lib.rules, lib.natives)){ - let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))] - const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) - if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){ - dlSize += (libItm.size*1) - libDlQueue.push(libItm) - } - } - cb() - }, (err) => { - self.libraries = new DLTracker(libDlQueue, dlSize) - resolve() - }) - }) - } - - // #endregion - - // Miscellaneous (Category=files) Validation Functions - // #region - - /** - * Public miscellaneous mojang file validation function. These files will be enqueued under - * the 'files' identifier. - * - * @param {Object} versionData The version data for the assets. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateMiscellaneous(versionData){ - const self = this - return new Promise(async (resolve, reject) => { - await self.validateClient(versionData) - await self.validateLogConfig(versionData) - resolve() - }) - } - - /** - * Validate client file - artifact renamed from client.jar to '{version}'.jar. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateClient(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - const clientData = versionData.downloads.client - const version = versionData.id - const targetPath = path.join(self.commonPath, 'versions', version) - const targetFile = version + '.jar' - - let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile)) - - if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){ - self.files.dlqueue.push(client) - self.files.dlsize += client.size*1 - resolve() - } else { - resolve() - } - }) - } - - /** - * Validate log config. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateLogConfig(versionData){ - const self = this - return new Promise((resolve, reject) => { - const client = versionData.logging.client - const file = client.file - const targetPath = path.join(self.commonPath, 'assets', 'log_configs') - - let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id)) - - if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){ - self.files.dlqueue.push(logConfig) - self.files.dlsize += logConfig.size*1 - resolve() - } else { - resolve() - } - }) - } - - // #endregion - - // Distribution (Category=forge) Validation Functions - // #region - - /** - * Validate the distribution. - * - * @param {Server} server The Server to validate. - * @returns {Promise.} A promise which resolves to the server distribution object. - */ - validateDistribution(server){ - const self = this - return new Promise((resolve, reject) => { - self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID()) - resolve(server) - }) - } - - _parseDistroModules(modules, version, servid){ - let alist = [] - let asize = 0 - for(let ob of modules){ - let obArtifact = ob.getArtifact() - let obPath = obArtifact.getPath() - let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, ob.getType()) - const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath - if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){ - asize += artifact.size*1 - alist.push(artifact) - if(validationPath !== obPath) this.extractQueue.push(obPath) - } - //Recursively process the submodules then combine the results. - if(ob.getSubModules() != null){ - let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid) - asize += dltrack.dlsize*1 - alist = alist.concat(dltrack.dlqueue) - } - } - - return new DLTracker(alist, asize) - } - - /** - * Loads Forge's version.json data into memory for the specified server id. - * - * @param {string} server The Server to load Forge data for. - * @returns {Promise.} A promise which resolves to Forge's version.json data. - */ - loadForgeData(server){ - const self = this - return new Promise(async (resolve, reject) => { - const modules = server.getModules() - for(let ob of modules){ - const type = ob.getType() - if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){ - if(Util.isForgeGradle3(server.getMinecraftVersion(), ob.getVersion())){ - // Read Manifest - for(let sub of ob.getSubModules()){ - if(sub.getType() === DistroManager.Types.VersionManifest){ - resolve(JSON.parse(fs.readFileSync(sub.getArtifact().getPath(), 'utf-8'))) - return - } - } - reject('No forge version manifest found!') - return - } else { - let obArtifact = ob.getArtifact() - let obPath = obArtifact.getPath() - let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type) - try { - let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath) - resolve(forgeData) - } catch (err){ - reject(err) - } - return - } - } - } - reject('No forge module found!') - }) - } - - _parseForgeLibraries(){ - /* TODO - * Forge asset validations are already implemented. When there's nothing much - * to work on, implement forge downloads using forge's version.json. This is to - * have the code on standby if we ever need it (since it's half implemented already). - */ - } - - // #endregion - - // Java (Category=''') Validation (download) Functions - // #region - - _enqueueOpenJDK(dataDir){ - return new Promise((resolve, reject) => { - JavaGuard._latestOpenJDK('8').then(verData => { - if(verData != null){ - - dataDir = path.join(dataDir, 'runtime', 'x64') - const fDir = path.join(dataDir, verData.name) - const jre = new Asset(verData.name, null, verData.size, verData.uri, fDir) - this.java = new DLTracker([jre], jre.size, (a, self) => { - if(verData.name.endsWith('zip')){ - - const zip = new AdmZip(a.to) - const pos = path.join(dataDir, zip.getEntries()[0].entryName) - zip.extractAllToAsync(dataDir, true, (err) => { - if(err){ - console.log(err) - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - } else { - fs.unlink(a.to, err => { - if(err){ - console.log(err) - } - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - }) - } - }) - - } else { - // Tar.gz - let h = null - fs.createReadStream(a.to) - .on('error', err => console.log(err)) - .pipe(zlib.createGunzip()) - .on('error', err => console.log(err)) - .pipe(tar.extract(dataDir, { - map: (header) => { - if(h == null){ - h = header.name - } - } - })) - .on('error', err => console.log(err)) - .on('finish', () => { - fs.unlink(a.to, err => { - if(err){ - console.log(err) - } - if(h.indexOf('/') > -1){ - h = h.substring(0, h.indexOf('/')) - } - const pos = path.join(dataDir, h) - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - }) - }) - } - }) - resolve(true) - - } else { - resolve(false) - } - }) - }) - - } - - // _enqueueOracleJRE(dataDir){ - // return new Promise((resolve, reject) => { - // JavaGuard._latestJREOracle().then(verData => { - // if(verData != null){ - - // const combined = verData.uri + PLATFORM_MAP[process.platform] - - // const opts = { - // url: combined, - // headers: { - // 'Cookie': 'oraclelicense=accept-securebackup-cookie' - // } - // } - - // request.head(opts, (err, resp, body) => { - // if(err){ - // resolve(false) - // } else { - // dataDir = path.join(dataDir, 'runtime', 'x64') - // const name = combined.substring(combined.lastIndexOf('/')+1) - // const fDir = path.join(dataDir, name) - // const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir) - // this.java = new DLTracker([jre], jre.size, (a, self) => { - // let h = null - // fs.createReadStream(a.to) - // .on('error', err => console.log(err)) - // .pipe(zlib.createGunzip()) - // .on('error', err => console.log(err)) - // .pipe(tar.extract(dataDir, { - // map: (header) => { - // if(h == null){ - // h = header.name - // } - // } - // })) - // .on('error', err => console.log(err)) - // .on('finish', () => { - // fs.unlink(a.to, err => { - // if(err){ - // console.log(err) - // } - // if(h.indexOf('/') > -1){ - // h = h.substring(0, h.indexOf('/')) - // } - // const pos = path.join(dataDir, h) - // self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - // }) - // }) - - // }) - // resolve(true) - // } - // }) - - // } else { - // resolve(false) - // } - // }) - // }) - - // } - - // _enqueueMojangJRE(dir){ - // return new Promise((resolve, reject) => { - // // Mojang does not host the JRE for linux. - // if(process.platform === 'linux'){ - // resolve(false) - // } - // AssetGuard.loadMojangLauncherData().then(data => { - // if(data != null) { - - // try { - // const mJRE = data[Library.mojangFriendlyOS()]['64'].jre - // const url = mJRE.url - - // request.head(url, (err, resp, body) => { - // if(err){ - // resolve(false) - // } else { - // const name = url.substring(url.lastIndexOf('/')+1) - // const fDir = path.join(dir, name) - // const jre = new Asset('jre' + mJRE.version, mJRE.sha1, resp.headers['content-length'], url, fDir) - // this.java = new DLTracker([jre], jre.size, a => { - // fs.readFile(a.to, (err, data) => { - // // Data buffer needs to be decompressed from lzma, - // // not really possible using node.js - // }) - // }) - // } - // }) - // } catch (err){ - // resolve(false) - // } - - // } - // }) - // }) - // } - - - // #endregion - - // #endregion - - // Control Flow Functions - // #region - - /** - * Initiate an async download process for an AssetGuard DLTracker. - * - * @param {string} identifier The identifier of the AssetGuard DLTracker. - * @param {number} limit Optional. The number of async processes to run in parallel. - * @returns {boolean} True if the process began, otherwise false. - */ - startAsyncProcess(identifier, limit = 5){ - - const self = this - const dlTracker = this[identifier] - const dlQueue = dlTracker.dlqueue - - if(dlQueue.length > 0){ - console.log('DLQueue', dlQueue) - - async.eachLimit(dlQueue, limit, (asset, cb) => { - - fs.ensureDirSync(path.join(asset.to, '..')) - - let req = request(asset.from) - req.pause() - - req.on('response', (resp) => { - - if(resp.statusCode === 200){ - - let doHashCheck = false - const contentLength = parseInt(resp.headers['content-length']) - - if(contentLength !== asset.size){ - console.log(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`) - doHashCheck = true - - // Adjust download - this.totaldlsize -= asset.size - this.totaldlsize += contentLength - } - - let writeStream = fs.createWriteStream(asset.to) - writeStream.on('close', () => { - if(dlTracker.callback != null){ - dlTracker.callback.apply(dlTracker, [asset, self]) - } - - if(doHashCheck){ - const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash) - if(v){ - console.log(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`) - } else { - console.error(`Hashes do not match, ${asset.id} may be corrupted.`) - } - } - - cb() - }) - req.pipe(writeStream) - req.resume() - - } else { - - req.abort() - console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`) - self.progress += asset.size*1 - self.emit('progress', 'download', self.progress, self.totaldlsize) - cb() - - } - - }) - - req.on('error', (err) => { - self.emit('error', 'download', err) - }) - - req.on('data', (chunk) => { - self.progress += chunk.length - self.emit('progress', 'download', self.progress, self.totaldlsize) - }) - - }, (err) => { - - if(err){ - console.log('An item in ' + identifier + ' failed to process') - } else { - console.log('All ' + identifier + ' have been processed successfully') - } - - //self.totaldlsize -= dlTracker.dlsize - //self.progress -= dlTracker.dlsize - self[identifier] = new DLTracker([], 0) - - if(self.progress >= self.totaldlsize) { - if(self.extractQueue.length > 0){ - self.emit('progress', 'extract', 1, 1) - //self.emit('extracting') - AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => { - self.extractQueue = [] - self.emit('complete', 'download') - }) - } else { - self.emit('complete', 'download') - } - } - - }) - - return true - - } else { - return false - } - } - - /** - * This function will initiate the download processed for the specified identifiers. If no argument is - * given, all identifiers will be initiated. Note that in order for files to be processed you need to run - * the processing function corresponding to that identifier. If you run this function without processing - * the files, it is likely nothing will be enqueued in the object and processing will complete - * immediately. Once all downloads are complete, this function will fire the 'complete' event on the - * global object instance. - * - * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit. - */ - processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){ - return new Promise((resolve, reject) => { - let shouldFire = true - - // Assign dltracking variables. - this.totaldlsize = 0 - this.progress = 0 - - for(let iden of identifiers){ - this.totaldlsize += this[iden.id].dlsize - } - - this.once('complete', (data) => { - resolve() - }) - - for(let iden of identifiers){ - let r = this.startAsyncProcess(iden.id, iden.limit) - if(r) shouldFire = false - } - - if(shouldFire){ - this.emit('complete', 'download') - } - }) - } - - async validateEverything(serverid, dev = false){ - - try { - if(!ConfigManager.isLoaded()){ - ConfigManager.load() - } - DistroManager.setDevMode(dev) - const dI = await DistroManager.pullLocal() - - const server = dI.getServer(serverid) - - // Validate Everything - - await this.validateDistribution(server) - this.emit('validate', 'distribution') - const versionData = await this.loadVersionData(server.getMinecraftVersion()) - this.emit('validate', 'version') - await this.validateAssets(versionData) - this.emit('validate', 'assets') - await this.validateLibraries(versionData) - this.emit('validate', 'libraries') - await this.validateMiscellaneous(versionData) - this.emit('validate', 'files') - await this.processDlQueues() - //this.emit('complete', 'download') - const forgeData = await this.loadForgeData(server) - - return { - versionData, - forgeData - } - - } catch (err){ - return { - versionData: null, - forgeData: null, - error: err - } - } - - - } - - // #endregion - -} - -module.exports = { - Util, - AssetGuard, - JavaGuard, - Asset, - Library +// Requirements +const AdmZip = require('adm-zip') +const async = require('async') +const child_process = require('child_process') +const crypto = require('crypto') +const EventEmitter = require('events') +const fs = require('fs-extra') +const path = require('path') +const Registry = require('winreg') +const request = require('request') +const tar = require('tar-fs') +const zlib = require('zlib') + +const ConfigManager = require('./configmanager') +const DistroManager = require('./distromanager') +const isDev = require('./isdev') + +// Constants +// const PLATFORM_MAP = { +// win32: '-windows-x64.tar.gz', +// darwin: '-macosx-x64.tar.gz', +// linux: '-linux-x64.tar.gz' +// } + +// Classes + +/** Class representing a base asset. */ +class Asset { + /** + * Create an asset. + * + * @param {any} id The id of the asset. + * @param {string} hash The hash value of the asset. + * @param {number} size The size in bytes of the asset. + * @param {string} from The url where the asset can be found. + * @param {string} to The absolute local file path of the asset. + */ + constructor(id, hash, size, from, to){ + this.id = id + this.hash = hash + this.size = size + this.from = from + this.to = to + } +} + +/** Class representing a mojang library. */ +class Library extends Asset { + + /** + * Converts the process.platform OS names to match mojang's OS names. + */ + static mojangFriendlyOS(){ + const opSys = process.platform + if (opSys === 'darwin') { + return 'osx' + } else if (opSys === 'win32'){ + return 'windows' + } else if (opSys === 'linux'){ + return 'linux' + } else { + return 'unknown_os' + } + } + + /** + * Checks whether or not a library is valid for download on a particular OS, following + * the rule format specified in the mojang version data index. If the allow property has + * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow + * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING + * the one specified. + * + * If the rules are undefined, the natives property will be checked for a matching entry + * for the current OS. + * + * @param {Array.} rules The Library's download rules. + * @param {Object} natives The Library's natives object. + * @returns {boolean} True if the Library follows the specified rules, otherwise false. + */ + static validateRules(rules, natives){ + if(rules == null) { + if(natives == null) { + return true + } else { + return natives[Library.mojangFriendlyOS()] != null + } + } + + for(let rule of rules){ + const action = rule.action + const osProp = rule.os + if(action != null && osProp != null){ + const osName = osProp.name + const osMoj = Library.mojangFriendlyOS() + if(action === 'allow'){ + return osName === osMoj + } else if(action === 'disallow'){ + return osName !== osMoj + } + } + } + return true + } +} + +class DistroModule extends Asset { + + /** + * Create a DistroModule. This is for processing, + * not equivalent to the module objects in the + * distro index. + * + * @param {any} id The id of the asset. + * @param {string} hash The hash value of the asset. + * @param {number} size The size in bytes of the asset. + * @param {string} from The url where the asset can be found. + * @param {string} to The absolute local file path of the asset. + * @param {string} type The the module type. + */ + constructor(id, hash, size, from, to, type){ + super(id, hash, size, from, to) + this.type = type + } + +} + +/** + * Class representing a download tracker. This is used to store meta data + * about a download queue, including the queue itself. + */ +class DLTracker { + + /** + * Create a DLTracker + * + * @param {Array.} dlqueue An array containing assets queued for download. + * @param {number} dlsize The combined size of each asset in the download queue array. + * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading. + */ + constructor(dlqueue, dlsize, callback = null){ + this.dlqueue = dlqueue + this.dlsize = dlsize + this.callback = callback + } + +} + +class Util { + + /** + * Returns true if the actual version is greater than + * or equal to the desired version. + * + * @param {string} desired The desired version. + * @param {string} actual The actual version. + */ + static mcVersionAtLeast(desired, actual){ + const des = desired.split('.') + const act = actual.split('.') + + for(let i=0; i= parseInt(des[i]))){ + return false + } + } + return true + } + + static isForgeGradle3(mcVersion, forgeVersion) { + + if(Util.mcVersionAtLeast('1.13', mcVersion)) { + return true + } + + try { + + const forgeVer = forgeVersion.split('-')[1] + + const maxFG2 = [14, 23, 5, 2847] + const verSplit = forgeVer.split('.').map(v => Number(v)) + + for(let i=0; i maxFG2[i]) { + return true + } else if(verSplit[i] < maxFG2[i]) { + return false + } + } + + return false + + } catch(err) { + throw new Error('Forge version is complex (changed).. launcher requires a patch.') + } + } + + static isAutoconnectBroken(forgeVersion) { + + const forgeVer = forgeVersion.split('-')[1] + + const minWorking = [31, 2, 15] + const verSplit = forgeVer.split('.').map(v => Number(v)) + + if(verSplit[0] === 31) { + for(let i=0; i minWorking[i]) { + return false + } else if(verSplit[i] < minWorking[i]) { + return true + } + } + } + + return false + } + +} + + +class JavaGuard extends EventEmitter { + + constructor(mcVersion){ + super() + this.mcVersion = mcVersion + } + + // /** + // * @typedef OracleJREData + // * @property {string} uri The base uri of the JRE. + // * @property {{major: string, update: string, build: string}} version Object containing version information. + // */ + + // /** + // * Resolves the latest version of Oracle's JRE and parses its download link. + // * + // * @returns {Promise.} Promise which resolved to an object containing the JRE download data. + // */ + // static _latestJREOracle(){ + + // const url = 'https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html' + // const regex = /https:\/\/.+?(?=\/java)\/java\/jdk\/([0-9]+u[0-9]+)-(b[0-9]+)\/([a-f0-9]{32})?\/jre-\1/ + + // return new Promise((resolve, reject) => { + // request(url, (err, resp, body) => { + // if(!err){ + // const arr = body.match(regex) + // const verSplit = arr[1].split('u') + // resolve({ + // uri: arr[0], + // version: { + // major: verSplit[0], + // update: verSplit[1], + // build: arr[2] + // } + // }) + // } else { + // resolve(null) + // } + // }) + // }) + // } + + /** + * @typedef OpenJDKData + * @property {string} uri The base uri of the JRE. + * @property {number} size The size of the download. + * @property {string} name The name of the artifact. + */ + + /** + * Fetch the last open JDK binary. Uses https://api.adoptopenjdk.net/ + * + * @param {string} major The major version of Java to fetch. + * + * @returns {Promise.} Promise which resolved to an object containing the JRE download data. + */ + static _latestOpenJDK(major = '8'){ + + const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform) + + const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre` + + return new Promise((resolve, reject) => { + request({url, json: true}, (err, resp, body) => { + if(!err && body.length > 0){ + resolve({ + uri: body[0].binary_link, + size: body[0].binary_size, + name: body[0].binary_name + }) + } else { + resolve(null) + } + }) + }) + } + + /** + * Returns the path of the OS-specific executable for the given Java + * installation. Supported OS's are win32, darwin, linux. + * + * @param {string} rootDir The root directory of the Java installation. + * @returns {string} The path to the Java executable. + */ + static javaExecFromRoot(rootDir){ + if(process.platform === 'win32'){ + return path.join(rootDir, 'bin', 'javaw.exe') + } else if(process.platform === 'darwin'){ + return path.join(rootDir, 'Contents', 'Home', 'bin', 'java') + } else if(process.platform === 'linux'){ + return path.join(rootDir, 'bin', 'java') + } + return rootDir + } + + /** + * Check to see if the given path points to a Java executable. + * + * @param {string} pth The path to check against. + * @returns {boolean} True if the path points to a Java executable, otherwise false. + */ + static isJavaExecPath(pth){ + if(process.platform === 'win32'){ + return pth.endsWith(path.join('bin', 'javaw.exe')) + } else if(process.platform === 'darwin'){ + return pth.endsWith(path.join('bin', 'java')) + } else if(process.platform === 'linux'){ + return pth.endsWith(path.join('bin', 'java')) + } + return false + } + + /** + * Load Mojang's launcher.json file. + * + * @returns {Promise.} Promise which resolves to Mojang's launcher.json object. + */ + static loadMojangLauncherData(){ + return new Promise((resolve, reject) => { + request.get('https://launchermeta.mojang.com/mc/launcher.json', (err, resp, body) => { + if(err){ + resolve(null) + } else { + resolve(JSON.parse(body)) + } + }) + }) + } + + /** + * Parses a **full** Java Runtime version string and resolves + * the version information. Dynamically detects the formatting + * to use. + * + * @param {string} verString Full version string to parse. + * @returns Object containing the version information. + */ + static parseJavaRuntimeVersion(verString){ + const major = verString.split('.')[0] + if(major == 1){ + return JavaGuard._parseJavaRuntimeVersion_8(verString) + } else { + return JavaGuard._parseJavaRuntimeVersion_9(verString) + } + } + + /** + * Parses a **full** Java Runtime version string and resolves + * the version information. Uses Java 8 formatting. + * + * @param {string} verString Full version string to parse. + * @returns Object containing the version information. + */ + static _parseJavaRuntimeVersion_8(verString){ + // 1.{major}.0_{update}-b{build} + // ex. 1.8.0_152-b16 + const ret = {} + let pts = verString.split('-') + ret.build = parseInt(pts[1].substring(1)) + pts = pts[0].split('_') + ret.update = parseInt(pts[1]) + ret.major = parseInt(pts[0].split('.')[1]) + return ret + } + + /** + * Parses a **full** Java Runtime version string and resolves + * the version information. Uses Java 9+ formatting. + * + * @param {string} verString Full version string to parse. + * @returns Object containing the version information. + */ + static _parseJavaRuntimeVersion_9(verString){ + // {major}.{minor}.{revision}+{build} + // ex. 10.0.2+13 + const ret = {} + let pts = verString.split('+') + ret.build = parseInt(pts[1]) + pts = pts[0].split('.') + ret.major = parseInt(pts[0]) + ret.minor = parseInt(pts[1]) + ret.revision = parseInt(pts[2]) + return ret + } + + /** + * Validates the output of a JVM's properties. Currently validates that a JRE is x64 + * and that the major = 8, update > 52. + * + * @param {string} stderr The output to validate. + * + * @returns {Promise.} A promise which resolves to a meta object about the JVM. + * The validity is stored inside the `valid` property. + */ + _validateJVMProperties(stderr){ + const res = stderr + const props = res.split('\n') + + const goal = 2 + let checksum = 0 + + const meta = {} + + for(let i=0; i -1){ + let arch = props[i].split('=')[1].trim() + arch = parseInt(arch) + console.log(props[i].trim()) + if(arch === 64){ + meta.arch = arch + ++checksum + if(checksum === goal){ + break + } + } + } else if(props[i].indexOf('java.runtime.version') > -1){ + let verString = props[i].split('=')[1].trim() + console.log(props[i].trim()) + const verOb = JavaGuard.parseJavaRuntimeVersion(verString) + if(verOb.major < 9){ + // Java 8 + if(verOb.major === 8 && verOb.update > 52){ + meta.version = verOb + ++checksum + if(checksum === goal){ + break + } + } + } else { + // Java 9+ + if(Util.mcVersionAtLeast('1.13', this.mcVersion)){ + console.log('Java 9+ not yet tested.') + /* meta.version = verOb + ++checksum + if(checksum === goal){ + break + } */ + } + } + } + } + + meta.valid = checksum === goal + + return meta + } + + /** + * Validates that a Java binary is at least 64 bit. This makes use of the non-standard + * command line option -XshowSettings:properties. The output of this contains a property, + * sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported + * in Java 8 and 9. Since this is a non-standard option. This will resolve to true if + * the function's code throws errors. That would indicate that the option is changed or + * removed. + * + * @param {string} binaryExecPath Path to the java executable we wish to validate. + * + * @returns {Promise.} A promise which resolves to a meta object about the JVM. + * The validity is stored inside the `valid` property. + */ + _validateJavaBinary(binaryExecPath){ + + return new Promise((resolve, reject) => { + if(!JavaGuard.isJavaExecPath(binaryExecPath)){ + resolve({valid: false}) + } else if(fs.existsSync(binaryExecPath)){ + // Workaround (javaw.exe no longer outputs this information.) + console.log(typeof binaryExecPath) + if(binaryExecPath.indexOf('javaw.exe') > -1) { + binaryExecPath.replace('javaw.exe', 'java.exe') + } + child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => { + try { + // Output is stored in stderr? + resolve(this._validateJVMProperties(stderr)) + } catch (err){ + // Output format might have changed, validation cannot be completed. + resolve({valid: false}) + } + }) + } else { + resolve({valid: false}) + } + }) + + } + + /** + * Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check + * to see if the value points to a path which exists. If the path exits, the path is returned. + * + * @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null. + */ + static _scanJavaHome(){ + const jHome = process.env.JAVA_HOME + try { + let res = fs.existsSync(jHome) + return res ? jHome : null + } catch (err) { + // Malformed JAVA_HOME property. + return null + } + } + + /** + * Scans the registry for 64-bit Java entries. The paths of each entry are added to + * a set and returned. Currently, only Java 8 (1.8) is supported. + * + * @returns {Promise.>} A promise which resolves to a set of 64-bit Java root + * paths found in the registry. + */ + static _scanRegistry(){ + + return new Promise((resolve, reject) => { + // Keys for Java v9.0.0 and later: + // 'SOFTWARE\\JavaSoft\\JRE' + // 'SOFTWARE\\JavaSoft\\JDK' + // Forge does not yet support Java 9, therefore we do not. + + // Keys for Java 1.8 and prior: + const regKeys = [ + '\\SOFTWARE\\JavaSoft\\Java Runtime Environment', + '\\SOFTWARE\\JavaSoft\\Java Development Kit' + ] + + let keysDone = 0 + + const candidates = new Set() + + for(let i=0; i { + if(exists) { + key.keys((err, javaVers) => { + if(err){ + keysDone++ + console.error(err) + + // REG KEY DONE + // DUE TO ERROR + if(keysDone === regKeys.length){ + resolve(candidates) + } + } else { + if(javaVers.length === 0){ + // REG KEY DONE + // NO SUBKEYS + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } else { + + let numDone = 0 + + for(let j=0; j { + const jHome = res.value + if(jHome.indexOf('(x86)') === -1){ + candidates.add(jHome) + } + + // SUBKEY DONE + + numDone++ + if(numDone === javaVers.length){ + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } + }) + } else { + + // SUBKEY DONE + // NOT JAVA 8 + + numDone++ + if(numDone === javaVers.length){ + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } + } + } + } + } + }) + } else { + + // REG KEY DONE + // DUE TO NON-EXISTANCE + + keysDone++ + if(keysDone === regKeys.length){ + resolve(candidates) + } + } + }) + } + + }) + + } + + /** + * See if JRE exists in the Internet Plug-Ins folder. + * + * @returns {string} The path of the JRE if found, otherwise null. + */ + static _scanInternetPlugins(){ + // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java + const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin' + const res = fs.existsSync(JavaGuard.javaExecFromRoot(pth)) + return res ? pth : null + } + + /** + * Scan a directory for root JVM folders. + * + * @param {string} scanDir The directory to scan. + * @returns {Promise.>} A promise which resolves to a set of the discovered + * root JVM folders. + */ + static _scanFileSystem(scanDir){ + return new Promise((resolve, reject) => { + + fs.exists(scanDir, (e) => { + + let res = new Set() + + if(e){ + fs.readdir(scanDir, (err, files) => { + if(err){ + resolve(res) + console.log(err) + } else { + let pathsDone = 0 + + for(let i=0; i { + + if(v){ + res.add(combinedPath) + } + + ++pathsDone + + if(pathsDone === files.length){ + resolve(res) + } + + }) + } + if(pathsDone === files.length){ + resolve(res) + } + } + }) + } else { + resolve(res) + } + }) + + }) + } + + /** + * + * @param {Set.} rootSet A set of JVM root strings to validate. + * @returns {Promise.} A promise which resolves to an array of meta objects + * for each valid JVM root directory. + */ + async _validateJavaRootSet(rootSet){ + + const rootArr = Array.from(rootSet) + const validArr = [] + + for(let i=0; i { + + if(a.version.major === b.version.major){ + + if(a.version.major < 9){ + // Java 8 + if(a.version.update === b.version.update){ + if(a.version.build === b.version.build){ + + // Same version, give priority to JRE. + if(a.execPath.toLowerCase().indexOf('jdk') > -1){ + return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 + } else { + return -1 + } + + } else { + return a.version.build > b.version.build ? -1 : 1 + } + } else { + return a.version.update > b.version.update ? -1 : 1 + } + } else { + // Java 9+ + if(a.version.minor === b.version.minor){ + if(a.version.revision === b.version.revision){ + + // Same version, give priority to JRE. + if(a.execPath.toLowerCase().indexOf('jdk') > -1){ + return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 + } else { + return -1 + } + + } else { + return a.version.revision > b.version.revision ? -1 : 1 + } + } else { + return a.version.minor > b.version.minor ? -1 : 1 + } + } + + } else { + return a.version.major > b.version.major ? -1 : 1 + } + }) + + return retArr + } + + /** + * Attempts to find a valid x64 installation of Java on Windows machines. + * Possible paths will be pulled from the registry and the JAVA_HOME environment + * variable. The paths will be sorted with higher versions preceeding lower, and + * JREs preceeding JDKs. The binaries at the sorted paths will then be validated. + * The first validated is returned. + * + * Higher versions > Lower versions + * If versions are equal, JRE > JDK. + * + * @param {string} dataDir The base launcher directory. + * @returns {Promise.} A Promise which resolves to the executable path of a valid + * x64 Java installation. If none are found, null is returned. + */ + async _win32JavaValidate(dataDir){ + + // Get possible paths from the registry. + let pathSet1 = await JavaGuard._scanRegistry() + if(pathSet1.length === 0){ + // Do a manual file system scan of program files. + pathSet1 = JavaGuard._scanFileSystem('C:\\Program Files\\Java') + } + + // Get possible paths from the data directory. + const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) + + // Merge the results. + const uberSet = new Set([...pathSet1, ...pathSet2]) + + // Validate JAVA_HOME. + const jHome = JavaGuard._scanJavaHome() + if(jHome != null && jHome.indexOf('(x86)') === -1){ + uberSet.add(jHome) + } + + let pathArr = await this._validateJavaRootSet(uberSet) + pathArr = JavaGuard._sortValidJavaArray(pathArr) + + if(pathArr.length > 0){ + return pathArr[0].execPath + } else { + return null + } + + } + + /** + * Attempts to find a valid x64 installation of Java on MacOS. + * The system JVM directory is scanned for possible installations. + * The JAVA_HOME enviroment variable and internet plugins directory + * are also scanned and validated. + * + * Higher versions > Lower versions + * If versions are equal, JRE > JDK. + * + * @param {string} dataDir The base launcher directory. + * @returns {Promise.} A Promise which resolves to the executable path of a valid + * x64 Java installation. If none are found, null is returned. + */ + async _darwinJavaValidate(dataDir){ + + const pathSet1 = await JavaGuard._scanFileSystem('/Library/Java/JavaVirtualMachines') + const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) + + const uberSet = new Set([...pathSet1, ...pathSet2]) + + // Check Internet Plugins folder. + const iPPath = JavaGuard._scanInternetPlugins() + if(iPPath != null){ + uberSet.add(iPPath) + } + + // Check the JAVA_HOME environment variable. + let jHome = JavaGuard._scanJavaHome() + if(jHome != null){ + // Ensure we are at the absolute root. + if(jHome.contains('/Contents/Home')){ + jHome = jHome.substring(0, jHome.indexOf('/Contents/Home')) + } + uberSet.add(jHome) + } + + let pathArr = await this._validateJavaRootSet(uberSet) + pathArr = JavaGuard._sortValidJavaArray(pathArr) + + if(pathArr.length > 0){ + return pathArr[0].execPath + } else { + return null + } + } + + /** + * Attempts to find a valid x64 installation of Java on Linux. + * The system JVM directory is scanned for possible installations. + * The JAVA_HOME enviroment variable is also scanned and validated. + * + * Higher versions > Lower versions + * If versions are equal, JRE > JDK. + * + * @param {string} dataDir The base launcher directory. + * @returns {Promise.} A Promise which resolves to the executable path of a valid + * x64 Java installation. If none are found, null is returned. + */ + async _linuxJavaValidate(dataDir){ + + const pathSet1 = await JavaGuard._scanFileSystem('/usr/lib/jvm') + const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) + + const uberSet = new Set([...pathSet1, ...pathSet2]) + + // Validate JAVA_HOME + const jHome = JavaGuard._scanJavaHome() + if(jHome != null){ + uberSet.add(jHome) + } + + let pathArr = await this._validateJavaRootSet(uberSet) + pathArr = JavaGuard._sortValidJavaArray(pathArr) + + if(pathArr.length > 0){ + return pathArr[0].execPath + } else { + return null + } + } + + /** + * Retrieve the path of a valid x64 Java installation. + * + * @param {string} dataDir The base launcher directory. + * @returns {string} A path to a valid x64 Java installation, null if none found. + */ + async validateJava(dataDir){ + return await this['_' + process.platform + 'JavaValidate'](dataDir) + } + +} + + + + +/** + * Central object class used for control flow. This object stores data about + * categories of downloads. Each category is assigned an identifier with a + * DLTracker object as its value. Combined information is also stored, such as + * the total size of all the queued files in each category. This event is used + * to emit events so that external modules can listen into processing done in + * this module. + */ +class AssetGuard extends EventEmitter { + + /** + * Create an instance of AssetGuard. + * On creation the object's properties are never-null default + * values. Each identifier is resolved to an empty DLTracker. + * + * @param {string} commonPath The common path for shared game files. + * @param {string} javaexec The path to a java executable which will be used + * to finalize installation. + */ + constructor(commonPath, javaexec){ + super() + this.totaldlsize = 0 + this.progress = 0 + this.assets = new DLTracker([], 0) + this.libraries = new DLTracker([], 0) + this.files = new DLTracker([], 0) + this.forge = new DLTracker([], 0) + this.java = new DLTracker([], 0) + this.extractQueue = [] + this.commonPath = commonPath + this.javaexec = javaexec + } + + // Static Utility Functions + // #region + + // Static Hash Validation Functions + // #region + + /** + * 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. + */ + static _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. + */ + static _parseChecksumsFile(content){ + let finalContent = {} + let lines = content.split('\n') + for(let i=0; i} checksums The checksums listed in the forge version index. + * @returns {boolean} True if the file exists and the hashes match, otherwise false. + */ + static _validateForgeChecksum(filePath, checksums){ + if(fs.existsSync(filePath)){ + if(checksums == null || checksums.length === 0){ + return true + } + let buf = fs.readFileSync(filePath) + let calcdhash = AssetGuard._calculateHash(buf, 'sha1') + let valid = checksums.includes(calcdhash) + if(!valid && filePath.endsWith('.jar')){ + valid = AssetGuard._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.} 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. + */ + static _validateForgeJar(buf, checksums){ + // Double pass method was the quickest I found. I tried a version where we store data + // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time. + + const hashes = {} + let expected = {} + + const zip = new AdmZip(buf) + const zipEntries = zip.getEntries() + + //First pass + for(let i=0; i} filePaths The paths of the files to be extracted and unpacked. + * @returns {Promise.} An empty promise to indicate the extraction has completed. + */ + static _extractPackXZ(filePaths, javaExecutable){ + console.log('[PackXZExtract] Starting') + return new Promise((resolve, reject) => { + + let libPath + if(isDev){ + libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') + } else { + if(process.platform === 'darwin'){ + libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar') + } else { + libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar') + } + } + + const filePath = filePaths.join(',') + const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath]) + child.stdout.on('data', (data) => { + console.log('[PackXZExtract]', data.toString('utf8')) + }) + child.stderr.on('data', (data) => { + console.log('[PackXZExtract]', data.toString('utf8')) + }) + child.on('close', (code, signal) => { + console.log('[PackXZExtract]', 'Exited with code', code) + resolve() + }) + }) + } + + /** + * Function which finalizes the forge installation process. This creates a 'version' + * instance for forge and saves its version.json file into that instance. If that + * instance already exists, the contents of the version.json file are read and returned + * in a promise. + * + * @param {Asset} asset The Asset object representing Forge. + * @param {string} commonPath The common path for shared game files. + * @returns {Promise.} A promise which resolves to the contents of forge's version.json. + */ + static _finalizeForgeAsset(asset, commonPath){ + return new Promise((resolve, reject) => { + fs.readFile(asset.to, (err, data) => { + const zip = new AdmZip(data) + const zipEntries = zip.getEntries() + + for(let i=0; i} Promise which resolves to the version data object. + */ + loadVersionData(version, force = false){ + const self = this + return new Promise(async (resolve, reject) => { + const versionPath = path.join(self.commonPath, 'versions', version) + const versionFile = path.join(versionPath, version + '.json') + if(!fs.existsSync(versionFile) || force){ + const url = await self._getVersionDataUrl(version) + //This download will never be tracked as it's essential and trivial. + console.log('Preparing download of ' + version + ' assets.') + fs.ensureDirSync(versionPath) + const stream = request(url).pipe(fs.createWriteStream(versionFile)) + stream.on('finish', () => { + resolve(JSON.parse(fs.readFileSync(versionFile))) + }) + } else { + resolve(JSON.parse(fs.readFileSync(versionFile))) + } + }) + } + + /** + * Parses Mojang's version manifest and retrieves the url of the version + * data index. + * + * @param {string} version The version to lookup. + * @returns {Promise.} Promise which resolves to the url of the version data index. + * If the version could not be found, resolves to null. + */ + _getVersionDataUrl(version){ + return new Promise((resolve, reject) => { + request('https://launchermeta.mojang.com/mc/game/version_manifest.json', (error, resp, body) => { + if(error){ + reject(error) + } else { + const manifest = JSON.parse(body) + + for(let v of manifest.versions){ + if(v.id === version){ + resolve(v.url) + } + } + + resolve(null) + } + }) + }) + } + + + // Asset (Category=''') Validation Functions + // #region + + /** + * Public asset validation function. This function will handle the validation of assets. + * It will parse the asset index specified in the version data, analyzing each + * asset entry. In this analysis it will check to see if the local file exists and is valid. + * If not, it will be added to the download queue for the 'assets' identifier. + * + * @param {Object} versionData The version data for the assets. + * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateAssets(versionData, force = false){ + const self = this + return new Promise((resolve, reject) => { + self._assetChainIndexData(versionData, force).then(() => { + resolve() + }) + }) + } + + //Chain the asset tasks to provide full async. The below functions are private. + /** + * Private function used to chain the asset validation process. This function retrieves + * the index data. + * @param {Object} versionData + * @param {boolean} force + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + _assetChainIndexData(versionData, force = false){ + const self = this + return new Promise((resolve, reject) => { + //Asset index constants. + const assetIndex = versionData.assetIndex + const name = assetIndex.id + '.json' + const indexPath = path.join(self.commonPath, 'assets', 'indexes') + const assetIndexLoc = path.join(indexPath, name) + + let data = null + if(!fs.existsSync(assetIndexLoc) || force){ + console.log('Downloading ' + versionData.id + ' asset index.') + fs.ensureDirSync(indexPath) + const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc)) + stream.on('finish', () => { + data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) + self._assetChainValidateAssets(versionData, data).then(() => { + resolve() + }) + }) + } else { + data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) + self._assetChainValidateAssets(versionData, data).then(() => { + resolve() + }) + } + }) + } + + /** + * Private function used to chain the asset validation process. This function processes + * the assets and enqueues missing or invalid files. + * @param {Object} versionData + * @param {boolean} force + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + _assetChainValidateAssets(versionData, indexData){ + const self = this + return new Promise((resolve, reject) => { + + //Asset constants + const resourceURL = 'http://resources.download.minecraft.net/' + const localPath = path.join(self.commonPath, 'assets') + const objectPath = path.join(localPath, 'objects') + + const assetDlQueue = [] + let dlSize = 0 + let acc = 0 + const total = Object.keys(indexData.objects).length + //const objKeys = Object.keys(data.objects) + async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => { + acc++ + self.emit('progress', 'assets', acc, total) + const hash = value.hash + const assetName = path.join(hash.substring(0, 2), hash) + const urlName = hash.substring(0, 2) + '/' + hash + const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName)) + if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){ + dlSize += (ast.size*1) + assetDlQueue.push(ast) + } + cb() + }, (err) => { + self.assets = new DLTracker(assetDlQueue, dlSize) + resolve() + }) + }) + } + + // #endregion + + // Library (Category=''') Validation Functions + // #region + + /** + * Public library validation function. This function will handle the validation of libraries. + * It will parse the version data, analyzing each library entry. In this analysis, it will + * check to see if the local file exists and is valid. If not, it will be added to the download + * queue for the 'libraries' identifier. + * + * @param {Object} versionData The version data for the assets. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateLibraries(versionData){ + const self = this + return new Promise((resolve, reject) => { + + const libArr = versionData.libraries + const libPath = path.join(self.commonPath, 'libraries') + + const libDlQueue = [] + let dlSize = 0 + + //Check validity of each library. If the hashs don't match, download the library. + async.eachLimit(libArr, 5, (lib, cb) => { + if(Library.validateRules(lib.rules, lib.natives)){ + let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))] + const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) + if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){ + dlSize += (libItm.size*1) + libDlQueue.push(libItm) + } + } + cb() + }, (err) => { + self.libraries = new DLTracker(libDlQueue, dlSize) + resolve() + }) + }) + } + + // #endregion + + // Miscellaneous (Category=files) Validation Functions + // #region + + /** + * Public miscellaneous mojang file validation function. These files will be enqueued under + * the 'files' identifier. + * + * @param {Object} versionData The version data for the assets. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateMiscellaneous(versionData){ + const self = this + return new Promise(async (resolve, reject) => { + await self.validateClient(versionData) + await self.validateLogConfig(versionData) + resolve() + }) + } + + /** + * Validate client file - artifact renamed from client.jar to '{version}'.jar. + * + * @param {Object} versionData The version data for the assets. + * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateClient(versionData, force = false){ + const self = this + return new Promise((resolve, reject) => { + const clientData = versionData.downloads.client + const version = versionData.id + const targetPath = path.join(self.commonPath, 'versions', version) + const targetFile = version + '.jar' + + let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile)) + + if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){ + self.files.dlqueue.push(client) + self.files.dlsize += client.size*1 + resolve() + } else { + resolve() + } + }) + } + + /** + * Validate log config. + * + * @param {Object} versionData The version data for the assets. + * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. + * @returns {Promise.} An empty promise to indicate the async processing has completed. + */ + validateLogConfig(versionData){ + const self = this + return new Promise((resolve, reject) => { + const client = versionData.logging.client + const file = client.file + const targetPath = path.join(self.commonPath, 'assets', 'log_configs') + + let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id)) + + if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){ + self.files.dlqueue.push(logConfig) + self.files.dlsize += logConfig.size*1 + resolve() + } else { + resolve() + } + }) + } + + // #endregion + + // Distribution (Category=forge) Validation Functions + // #region + + /** + * Validate the distribution. + * + * @param {Server} server The Server to validate. + * @returns {Promise.} A promise which resolves to the server distribution object. + */ + validateDistribution(server){ + const self = this + return new Promise((resolve, reject) => { + self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID()) + resolve(server) + }) + } + + _parseDistroModules(modules, version, servid){ + let alist = [] + let asize = 0 + for(let ob of modules){ + let obArtifact = ob.getArtifact() + let obPath = obArtifact.getPath() + let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, ob.getType()) + const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath + if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){ + asize += artifact.size*1 + alist.push(artifact) + if(validationPath !== obPath) this.extractQueue.push(obPath) + } + //Recursively process the submodules then combine the results. + if(ob.getSubModules() != null){ + let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid) + asize += dltrack.dlsize*1 + alist = alist.concat(dltrack.dlqueue) + } + } + + return new DLTracker(alist, asize) + } + + /** + * Loads Forge's version.json data into memory for the specified server id. + * + * @param {string} server The Server to load Forge data for. + * @returns {Promise.} A promise which resolves to Forge's version.json data. + */ + loadForgeData(server){ + const self = this + return new Promise(async (resolve, reject) => { + const modules = server.getModules() + for(let ob of modules){ + const type = ob.getType() + if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){ + if(Util.isForgeGradle3(server.getMinecraftVersion(), ob.getVersion())){ + // Read Manifest + for(let sub of ob.getSubModules()){ + if(sub.getType() === DistroManager.Types.VersionManifest){ + resolve(JSON.parse(fs.readFileSync(sub.getArtifact().getPath(), 'utf-8'))) + return + } + } + reject('No forge version manifest found!') + return + } else { + let obArtifact = ob.getArtifact() + let obPath = obArtifact.getPath() + let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type) + try { + let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath) + resolve(forgeData) + } catch (err){ + reject(err) + } + return + } + } + } + reject('No forge module found!') + }) + } + + _parseForgeLibraries(){ + /* TODO + * Forge asset validations are already implemented. When there's nothing much + * to work on, implement forge downloads using forge's version.json. This is to + * have the code on standby if we ever need it (since it's half implemented already). + */ + } + + // #endregion + + // Java (Category=''') Validation (download) Functions + // #region + + _enqueueOpenJDK(dataDir){ + return new Promise((resolve, reject) => { + JavaGuard._latestOpenJDK('8').then(verData => { + if(verData != null){ + + dataDir = path.join(dataDir, 'runtime', 'x64') + const fDir = path.join(dataDir, verData.name) + const jre = new Asset(verData.name, null, verData.size, verData.uri, fDir) + this.java = new DLTracker([jre], jre.size, (a, self) => { + if(verData.name.endsWith('zip')){ + + const zip = new AdmZip(a.to) + const pos = path.join(dataDir, zip.getEntries()[0].entryName) + zip.extractAllToAsync(dataDir, true, (err) => { + if(err){ + console.log(err) + self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + } else { + fs.unlink(a.to, err => { + if(err){ + console.log(err) + } + self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + }) + } + }) + + } else { + // Tar.gz + let h = null + fs.createReadStream(a.to) + .on('error', err => console.log(err)) + .pipe(zlib.createGunzip()) + .on('error', err => console.log(err)) + .pipe(tar.extract(dataDir, { + map: (header) => { + if(h == null){ + h = header.name + } + } + })) + .on('error', err => console.log(err)) + .on('finish', () => { + fs.unlink(a.to, err => { + if(err){ + console.log(err) + } + if(h.indexOf('/') > -1){ + h = h.substring(0, h.indexOf('/')) + } + const pos = path.join(dataDir, h) + self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + }) + }) + } + }) + resolve(true) + + } else { + resolve(false) + } + }) + }) + + } + + // _enqueueOracleJRE(dataDir){ + // return new Promise((resolve, reject) => { + // JavaGuard._latestJREOracle().then(verData => { + // if(verData != null){ + + // const combined = verData.uri + PLATFORM_MAP[process.platform] + + // const opts = { + // url: combined, + // headers: { + // 'Cookie': 'oraclelicense=accept-securebackup-cookie' + // } + // } + + // request.head(opts, (err, resp, body) => { + // if(err){ + // resolve(false) + // } else { + // dataDir = path.join(dataDir, 'runtime', 'x64') + // const name = combined.substring(combined.lastIndexOf('/')+1) + // const fDir = path.join(dataDir, name) + // const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir) + // this.java = new DLTracker([jre], jre.size, (a, self) => { + // let h = null + // fs.createReadStream(a.to) + // .on('error', err => console.log(err)) + // .pipe(zlib.createGunzip()) + // .on('error', err => console.log(err)) + // .pipe(tar.extract(dataDir, { + // map: (header) => { + // if(h == null){ + // h = header.name + // } + // } + // })) + // .on('error', err => console.log(err)) + // .on('finish', () => { + // fs.unlink(a.to, err => { + // if(err){ + // console.log(err) + // } + // if(h.indexOf('/') > -1){ + // h = h.substring(0, h.indexOf('/')) + // } + // const pos = path.join(dataDir, h) + // self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) + // }) + // }) + + // }) + // resolve(true) + // } + // }) + + // } else { + // resolve(false) + // } + // }) + // }) + + // } + + // _enqueueMojangJRE(dir){ + // return new Promise((resolve, reject) => { + // // Mojang does not host the JRE for linux. + // if(process.platform === 'linux'){ + // resolve(false) + // } + // AssetGuard.loadMojangLauncherData().then(data => { + // if(data != null) { + + // try { + // const mJRE = data[Library.mojangFriendlyOS()]['64'].jre + // const url = mJRE.url + + // request.head(url, (err, resp, body) => { + // if(err){ + // resolve(false) + // } else { + // const name = url.substring(url.lastIndexOf('/')+1) + // const fDir = path.join(dir, name) + // const jre = new Asset('jre' + mJRE.version, mJRE.sha1, resp.headers['content-length'], url, fDir) + // this.java = new DLTracker([jre], jre.size, a => { + // fs.readFile(a.to, (err, data) => { + // // Data buffer needs to be decompressed from lzma, + // // not really possible using node.js + // }) + // }) + // } + // }) + // } catch (err){ + // resolve(false) + // } + + // } + // }) + // }) + // } + + + // #endregion + + // #endregion + + // Control Flow Functions + // #region + + /** + * Initiate an async download process for an AssetGuard DLTracker. + * + * @param {string} identifier The identifier of the AssetGuard DLTracker. + * @param {number} limit Optional. The number of async processes to run in parallel. + * @returns {boolean} True if the process began, otherwise false. + */ + startAsyncProcess(identifier, limit = 5){ + + const self = this + const dlTracker = this[identifier] + const dlQueue = dlTracker.dlqueue + + if(dlQueue.length > 0){ + console.log('DLQueue', dlQueue) + + async.eachLimit(dlQueue, limit, (asset, cb) => { + + fs.ensureDirSync(path.join(asset.to, '..')) + + let req = request(asset.from) + req.pause() + + req.on('response', (resp) => { + + if(resp.statusCode === 200){ + + let doHashCheck = false + const contentLength = parseInt(resp.headers['content-length']) + + if(contentLength !== asset.size){ + console.log(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`) + doHashCheck = true + + // Adjust download + this.totaldlsize -= asset.size + this.totaldlsize += contentLength + } + + let writeStream = fs.createWriteStream(asset.to) + writeStream.on('close', () => { + if(dlTracker.callback != null){ + dlTracker.callback.apply(dlTracker, [asset, self]) + } + + if(doHashCheck){ + const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash) + if(v){ + console.log(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`) + } else { + console.error(`Hashes do not match, ${asset.id} may be corrupted.`) + } + } + + cb() + }) + req.pipe(writeStream) + req.resume() + + } else { + + req.abort() + console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`) + self.progress += asset.size*1 + self.emit('progress', 'download', self.progress, self.totaldlsize) + cb() + + } + + }) + + req.on('error', (err) => { + self.emit('error', 'download', err) + }) + + req.on('data', (chunk) => { + self.progress += chunk.length + self.emit('progress', 'download', self.progress, self.totaldlsize) + }) + + }, (err) => { + + if(err){ + console.log('An item in ' + identifier + ' failed to process') + } else { + console.log('All ' + identifier + ' have been processed successfully') + } + + //self.totaldlsize -= dlTracker.dlsize + //self.progress -= dlTracker.dlsize + self[identifier] = new DLTracker([], 0) + + if(self.progress >= self.totaldlsize) { + if(self.extractQueue.length > 0){ + self.emit('progress', 'extract', 1, 1) + //self.emit('extracting') + AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => { + self.extractQueue = [] + self.emit('complete', 'download') + }) + } else { + self.emit('complete', 'download') + } + } + + }) + + return true + + } else { + return false + } + } + + /** + * This function will initiate the download processed for the specified identifiers. If no argument is + * given, all identifiers will be initiated. Note that in order for files to be processed you need to run + * the processing function corresponding to that identifier. If you run this function without processing + * the files, it is likely nothing will be enqueued in the object and processing will complete + * immediately. Once all downloads are complete, this function will fire the 'complete' event on the + * global object instance. + * + * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit. + */ + processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){ + return new Promise((resolve, reject) => { + let shouldFire = true + + // Assign dltracking variables. + this.totaldlsize = 0 + this.progress = 0 + + for(let iden of identifiers){ + this.totaldlsize += this[iden.id].dlsize + } + + this.once('complete', (data) => { + resolve() + }) + + for(let iden of identifiers){ + let r = this.startAsyncProcess(iden.id, iden.limit) + if(r) shouldFire = false + } + + if(shouldFire){ + this.emit('complete', 'download') + } + }) + } + + async validateEverything(serverid, dev = false){ + + try { + if(!ConfigManager.isLoaded()){ + ConfigManager.load() + } + DistroManager.setDevMode(dev) + const dI = await DistroManager.pullLocal() + + const server = dI.getServer(serverid) + + // Validate Everything + + await this.validateDistribution(server) + this.emit('validate', 'distribution') + const versionData = await this.loadVersionData(server.getMinecraftVersion()) + this.emit('validate', 'version') + await this.validateAssets(versionData) + this.emit('validate', 'assets') + await this.validateLibraries(versionData) + this.emit('validate', 'libraries') + await this.validateMiscellaneous(versionData) + this.emit('validate', 'files') + await this.processDlQueues() + //this.emit('complete', 'download') + const forgeData = await this.loadForgeData(server) + + return { + versionData, + forgeData + } + + } catch (err){ + return { + versionData: null, + forgeData: null, + error: err + } + } + + + } + + // #endregion + +} + +module.exports = { + Util, + AssetGuard, + JavaGuard, + Asset, + Library } \ No newline at end of file diff --git a/app/assets/js/authmanager.js b/app/assets/js/authmanager.js index 22b2fed9..28761b3e 100644 --- a/app/assets/js/authmanager.js +++ b/app/assets/js/authmanager.js @@ -1,99 +1,99 @@ -/** - * AuthManager - * - * This module aims to abstract login procedures. Results from Mojang's REST api - * are retrieved through our Mojang module. These results are processed and stored, - * if applicable, in the config using the ConfigManager. All login procedures should - * be made through this module. - * - * @module authmanager - */ -// Requirements -const ConfigManager = require('./configmanager') -const LoggerUtil = require('./loggerutil') -const Mojang = require('./mojang') -const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold') -const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold') - -// Functions - -/** - * Add an account. This will authenticate the given credentials with Mojang's - * authserver. The resultant data will be stored as an auth account in the - * configuration database. - * - * @param {string} username The account username (email if migrated). - * @param {string} password The account password. - * @returns {Promise.} Promise which resolves the resolved authenticated account object. - */ -exports.addAccount = async function(username, password){ - try { - const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken()) - if(session.selectedProfile != null){ - const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) - if(ConfigManager.getClientToken() == null){ - ConfigManager.setClientToken(session.clientToken) - } - ConfigManager.save() - return ret - } else { - throw new Error('NotPaidAccount') - } - - } catch (err){ - return Promise.reject(err) - } -} - -/** - * Remove an account. This will invalidate the access token associated - * with the account and then remove it from the database. - * - * @param {string} uuid The UUID of the account to be removed. - * @returns {Promise.} Promise which resolves to void when the action is complete. - */ -exports.removeAccount = async function(uuid){ - try { - const authAcc = ConfigManager.getAuthAccount(uuid) - await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) - ConfigManager.removeAuthAccount(uuid) - ConfigManager.save() - return Promise.resolve() - } catch (err){ - return Promise.reject(err) - } -} - -/** - * Validate the selected account with Mojang's authserver. If the account is not valid, - * we will attempt to refresh the access token and update that value. If that fails, a - * new login will be required. - * - * **Function is WIP** - * - * @returns {Promise.} Promise which resolves to true if the access token is valid, - * otherwise false. - */ -exports.validateSelected = async function(){ - const current = ConfigManager.getSelectedAccount() - const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken()) - if(!isValid){ - try { - const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken()) - ConfigManager.updateAuthAccount(current.uuid, session.accessToken) - ConfigManager.save() - } catch(err) { - logger.debug('Error while validating selected profile:', err) - if(err && err.error === 'ForbiddenOperationException'){ - // What do we do? - } - logger.log('Account access token is invalid.') - return false - } - loggerSuccess.log('Account access token validated.') - return true - } else { - loggerSuccess.log('Account access token validated.') - return true - } +/** + * AuthManager + * + * This module aims to abstract login procedures. Results from Mojang's REST api + * are retrieved through our Mojang module. These results are processed and stored, + * if applicable, in the config using the ConfigManager. All login procedures should + * be made through this module. + * + * @module authmanager + */ +// Requirements +const ConfigManager = require('./configmanager') +const LoggerUtil = require('./loggerutil') +const Mojang = require('./mojang') +const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold') +const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold') + +// Functions + +/** + * Add an account. This will authenticate the given credentials with Mojang's + * authserver. The resultant data will be stored as an auth account in the + * configuration database. + * + * @param {string} username The account username (email if migrated). + * @param {string} password The account password. + * @returns {Promise.} Promise which resolves the resolved authenticated account object. + */ +exports.addAccount = async function(username, password){ + try { + const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken()) + if(session.selectedProfile != null){ + const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) + if(ConfigManager.getClientToken() == null){ + ConfigManager.setClientToken(session.clientToken) + } + ConfigManager.save() + return ret + } else { + throw new Error('NotPaidAccount') + } + + } catch (err){ + return Promise.reject(err) + } +} + +/** + * Remove an account. This will invalidate the access token associated + * with the account and then remove it from the database. + * + * @param {string} uuid The UUID of the account to be removed. + * @returns {Promise.} Promise which resolves to void when the action is complete. + */ +exports.removeAccount = async function(uuid){ + try { + const authAcc = ConfigManager.getAuthAccount(uuid) + await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) + ConfigManager.removeAuthAccount(uuid) + ConfigManager.save() + return Promise.resolve() + } catch (err){ + return Promise.reject(err) + } +} + +/** + * Validate the selected account with Mojang's authserver. If the account is not valid, + * we will attempt to refresh the access token and update that value. If that fails, a + * new login will be required. + * + * **Function is WIP** + * + * @returns {Promise.} Promise which resolves to true if the access token is valid, + * otherwise false. + */ +exports.validateSelected = async function(){ + const current = ConfigManager.getSelectedAccount() + const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken()) + if(!isValid){ + try { + const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken()) + ConfigManager.updateAuthAccount(current.uuid, session.accessToken) + ConfigManager.save() + } catch(err) { + logger.debug('Error while validating selected profile:', err) + if(err && err.error === 'ForbiddenOperationException'){ + // What do we do? + } + logger.log('Account access token is invalid.') + return false + } + loggerSuccess.log('Account access token validated.') + return true + } else { + loggerSuccess.log('Account access token validated.') + return true + } } \ No newline at end of file diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js index 65a73061..acfb3356 100644 --- a/app/assets/js/configmanager.js +++ b/app/assets/js/configmanager.js @@ -1,688 +1,688 @@ -const fs = require('fs-extra') -const os = require('os') -const path = require('path') - -const logger = require('./loggerutil')('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold') - -const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) -// TODO change -const dataPath = path.join(sysRoot, '.helioslauncher') - -// Forked processes do not have access to electron, so we have this workaround. -const launcherDir = process.env.CONFIG_DIRECT_PATH || require('electron').remote.app.getPath('userData') - -/** - * Retrieve the absolute path of the launcher directory. - * - * @returns {string} The absolute path of the launcher directory. - */ -exports.getLauncherDirectory = function(){ - return launcherDir -} - -/** - * Get the launcher's data directory. This is where all files related - * to game launch are installed (common, instances, java, etc). - * - * @returns {string} The absolute path of the launcher's data directory. - */ -exports.getDataDirectory = function(def = false){ - return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory -} - -/** - * Set the new data directory. - * - * @param {string} dataDirectory The new data directory. - */ -exports.setDataDirectory = function(dataDirectory){ - config.settings.launcher.dataDirectory = dataDirectory -} - -const configPath = path.join(exports.getLauncherDirectory(), 'config.json') -const configPathLEGACY = path.join(dataPath, 'config.json') -const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) - -exports.getAbsoluteMinRAM = function(){ - const mem = os.totalmem() - return mem >= 6000000000 ? 3 : 2 -} - -exports.getAbsoluteMaxRAM = function(){ - const mem = os.totalmem() - const gT16 = mem-16000000000 - return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8) + 16000000000/4) : mem/4))/1000000000) -} - -function resolveMaxRAM(){ - const mem = os.totalmem() - return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') -} - -function resolveMinRAM(){ - return resolveMaxRAM() -} - -/** - * Three types of values: - * Static = Explicitly declared. - * Dynamic = Calculated by a private function. - * Resolved = Resolved externally, defaults to null. - */ -const DEFAULT_CONFIG = { - settings: { - java: { - minRAM: resolveMinRAM(), - maxRAM: resolveMaxRAM(), // Dynamic - executable: null, - jvmOptions: [ - '-XX:+UseConcMarkSweepGC', - '-XX:+CMSIncrementalMode', - '-XX:-UseAdaptiveSizePolicy', - '-Xmn128M' - ], - }, - game: { - resWidth: 1280, - resHeight: 720, - fullscreen: false, - autoConnect: true, - launchDetached: true - }, - launcher: { - allowPrerelease: false, - dataDirectory: dataPath - } - }, - newsCache: { - date: null, - content: null, - dismissed: false - }, - clientToken: null, - selectedServer: null, // Resolved - selectedAccount: null, - authenticationDatabase: {}, - modConfigurations: [] -} - -let config = null - -// Persistance Utility Functions - -/** - * Save the current configuration to a file. - */ -exports.save = function(){ - fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') -} - -/** - * Load the configuration into memory. If a configuration file exists, - * that will be read and saved. Otherwise, a default configuration will - * be generated. Note that "resolved" values default to null and will - * need to be externally assigned. - */ -exports.load = function(){ - let doLoad = true - - if(!fs.existsSync(configPath)){ - // Create all parent directories. - fs.ensureDirSync(path.join(configPath, '..')) - if(fs.existsSync(configPathLEGACY)){ - fs.moveSync(configPathLEGACY, configPath) - } else { - doLoad = false - config = DEFAULT_CONFIG - exports.save() - } - } - if(doLoad){ - let doValidate = false - try { - config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) - doValidate = true - } catch (err){ - logger.error(err) - logger.log('Configuration file contains malformed JSON or is corrupt.') - logger.log('Generating a new configuration file.') - fs.ensureDirSync(path.join(configPath, '..')) - config = DEFAULT_CONFIG - exports.save() - } - if(doValidate){ - config = validateKeySet(DEFAULT_CONFIG, config) - exports.save() - } - } - logger.log('Successfully Loaded') -} - -/** - * @returns {boolean} Whether or not the manager has been loaded. - */ -exports.isLoaded = function(){ - return config != null -} - -/** - * Validate that the destination object has at least every field - * present in the source object. Assign a default value otherwise. - * - * @param {Object} srcObj The source object to reference against. - * @param {Object} destObj The destination object. - * @returns {Object} A validated destination object. - */ -function validateKeySet(srcObj, destObj){ - if(srcObj == null){ - srcObj = {} - } - const validationBlacklist = ['authenticationDatabase'] - const keys = Object.keys(srcObj) - for(let i=0; i} An array of each stored authenticated account. - */ -exports.getAuthAccounts = function(){ - return config.authenticationDatabase -} - -/** - * Returns the authenticated account with the given uuid. Value may - * be null. - * - * @param {string} uuid The uuid of the authenticated account. - * @returns {Object} The authenticated account with the given uuid. - */ -exports.getAuthAccount = function(uuid){ - return config.authenticationDatabase[uuid] -} - -/** - * Update the access token of an authenticated account. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The new Access Token. - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.updateAuthAccount = function(uuid, accessToken){ - config.authenticationDatabase[uuid].accessToken = accessToken - return config.authenticationDatabase[uuid] -} - -/** - * Adds an authenticated account to the database to be stored. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The accessToken of the authenticated account. - * @param {string} username The username (usually email) of the authenticated account. - * @param {string} displayName The in game name of the authenticated account. - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.addAuthAccount = function(uuid, accessToken, username, displayName){ - config.selectedAccount = uuid - config.authenticationDatabase[uuid] = { - accessToken, - username: username.trim(), - uuid: uuid.trim(), - displayName: displayName.trim() - } - return config.authenticationDatabase[uuid] -} - -/** - * Remove an authenticated account from the database. If the account - * was also the selected account, a new one will be selected. If there - * are no accounts, the selected account will be null. - * - * @param {string} uuid The uuid of the authenticated account. - * - * @returns {boolean} True if the account was removed, false if it never existed. - */ -exports.removeAuthAccount = function(uuid){ - if(config.authenticationDatabase[uuid] != null){ - delete config.authenticationDatabase[uuid] - if(config.selectedAccount === uuid){ - const keys = Object.keys(config.authenticationDatabase) - if(keys.length > 0){ - config.selectedAccount = keys[0] - } else { - config.selectedAccount = null - config.clientToken = null - } - } - return true - } - return false -} - -/** - * Get the currently selected authenticated account. - * - * @returns {Object} The selected authenticated account. - */ -exports.getSelectedAccount = function(){ - return config.authenticationDatabase[config.selectedAccount] -} - -/** - * Set the selected authenticated account. - * - * @param {string} uuid The UUID of the account which is to be set - * as the selected account. - * - * @returns {Object} The selected authenticated account. - */ -exports.setSelectedAccount = function(uuid){ - const authAcc = config.authenticationDatabase[uuid] - if(authAcc != null) { - config.selectedAccount = uuid - } - return authAcc -} - -/** - * Get an array of each mod configuration currently stored. - * - * @returns {Array.} An array of each stored mod configuration. - */ -exports.getModConfigurations = function(){ - return config.modConfigurations -} - -/** - * Set the array of stored mod configurations. - * - * @param {Array.} configurations An array of mod configurations. - */ -exports.setModConfigurations = function(configurations){ - config.modConfigurations = configurations -} - -/** - * Get the mod configuration for a specific server. - * - * @param {string} serverid The id of the server. - * @returns {Object} The mod configuration for the given server. - */ -exports.getModConfiguration = function(serverid){ - const cfgs = config.modConfigurations - for(let i=0; i} An array of the additional arguments for JVM initialization. - */ -exports.getJVMOptions = function(def = false){ - return !def ? config.settings.java.jvmOptions : DEFAULT_CONFIG.settings.java.jvmOptions -} - -/** - * Set the additional arguments for JVM initialization. Required arguments, - * such as memory allocation, will be dynamically resolved and should not be - * included in this value. - * - * @param {Array.} jvmOptions An array of the new additional arguments for JVM - * initialization. - */ -exports.setJVMOptions = function(jvmOptions){ - config.settings.java.jvmOptions = jvmOptions -} - -// Game Settings - -/** - * Retrieve the width of the game window. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {number} The width of the game window. - */ -exports.getGameWidth = function(def = false){ - return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth -} - -/** - * Set the width of the game window. - * - * @param {number} resWidth The new width of the game window. - */ -exports.setGameWidth = function(resWidth){ - config.settings.game.resWidth = Number.parseInt(resWidth) -} - -/** - * Validate a potential new width value. - * - * @param {number} resWidth The width value to validate. - * @returns {boolean} Whether or not the value is valid. - */ -exports.validateGameWidth = function(resWidth){ - const nVal = Number.parseInt(resWidth) - return Number.isInteger(nVal) && nVal >= 0 -} - -/** - * Retrieve the height of the game window. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {number} The height of the game window. - */ -exports.getGameHeight = function(def = false){ - return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight -} - -/** - * Set the height of the game window. - * - * @param {number} resHeight The new height of the game window. - */ -exports.setGameHeight = function(resHeight){ - config.settings.game.resHeight = Number.parseInt(resHeight) -} - -/** - * Validate a potential new height value. - * - * @param {number} resHeight The height value to validate. - * @returns {boolean} Whether or not the value is valid. - */ -exports.validateGameHeight = function(resHeight){ - const nVal = Number.parseInt(resHeight) - return Number.isInteger(nVal) && nVal >= 0 -} - -/** - * Check if the game should be launched in fullscreen mode. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game is set to launch in fullscreen mode. - */ -exports.getFullscreen = function(def = false){ - return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen -} - -/** - * Change the status of if the game should be launched in fullscreen mode. - * - * @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode. - */ -exports.setFullscreen = function(fullscreen){ - config.settings.game.fullscreen = fullscreen -} - -/** - * Check if the game should auto connect to servers. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game should auto connect to servers. - */ -exports.getAutoConnect = function(def = false){ - return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect -} - -/** - * Change the status of whether or not the game should auto connect to servers. - * - * @param {boolean} autoConnect Whether or not the game should auto connect to servers. - */ -exports.setAutoConnect = function(autoConnect){ - config.settings.game.autoConnect = autoConnect -} - -/** - * Check if the game should launch as a detached process. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game will launch as a detached process. - */ -exports.getLaunchDetached = function(def = false){ - return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached -} - -/** - * Change the status of whether or not the game should launch as a detached process. - * - * @param {boolean} launchDetached Whether or not the game should launch as a detached process. - */ -exports.setLaunchDetached = function(launchDetached){ - config.settings.game.launchDetached = launchDetached -} - -// Launcher Settings - -/** - * Check if the launcher should download prerelease versions. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the launcher should download prerelease versions. - */ -exports.getAllowPrerelease = function(def = false){ - return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease -} - -/** - * Change the status of Whether or not the launcher should download prerelease versions. - * - * @param {boolean} launchDetached Whether or not the launcher should download prerelease versions. - */ -exports.setAllowPrerelease = function(allowPrerelease){ - config.settings.launcher.allowPrerelease = allowPrerelease +const fs = require('fs-extra') +const os = require('os') +const path = require('path') + +const logger = require('./loggerutil')('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold') + +const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) +// TODO change +const dataPath = path.join(sysRoot, '.nemesismclauncher') + +// Forked processes do not have access to electron, so we have this workaround. +const launcherDir = process.env.CONFIG_DIRECT_PATH || require('electron').remote.app.getPath('userData') + +/** + * Retrieve the absolute path of the launcher directory. + * + * @returns {string} The absolute path of the launcher directory. + */ +exports.getLauncherDirectory = function(){ + return launcherDir +} + +/** + * Get the launcher's data directory. This is where all files related + * to game launch are installed (common, instances, java, etc). + * + * @returns {string} The absolute path of the launcher's data directory. + */ +exports.getDataDirectory = function(def = false){ + return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory +} + +/** + * Set the new data directory. + * + * @param {string} dataDirectory The new data directory. + */ +exports.setDataDirectory = function(dataDirectory){ + config.settings.launcher.dataDirectory = dataDirectory +} + +const configPath = path.join(exports.getLauncherDirectory(), 'config.json') +const configPathLEGACY = path.join(dataPath, 'config.json') +const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) + +exports.getAbsoluteMinRAM = function(){ + const mem = os.totalmem() + return mem >= 6000000000 ? 3 : 2 +} + +exports.getAbsoluteMaxRAM = function(){ + const mem = os.totalmem() + const gT16 = mem-16000000000 + return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8) + 16000000000/4) : mem/4))/1000000000) +} + +function resolveMaxRAM(){ + const mem = os.totalmem() + return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') +} + +function resolveMinRAM(){ + return resolveMaxRAM() +} + +/** + * Three types of values: + * Static = Explicitly declared. + * Dynamic = Calculated by a private function. + * Resolved = Resolved externally, defaults to null. + */ +const DEFAULT_CONFIG = { + settings: { + java: { + minRAM: resolveMinRAM(), + maxRAM: resolveMaxRAM(), // Dynamic + executable: null, + jvmOptions: [ + '-XX:+UseConcMarkSweepGC', + '-XX:+CMSIncrementalMode', + '-XX:-UseAdaptiveSizePolicy', + '-Xmn128M' + ], + }, + game: { + resWidth: 1280, + resHeight: 720, + fullscreen: false, + autoConnect: true, + launchDetached: true + }, + launcher: { + allowPrerelease: false, + dataDirectory: dataPath + } + }, + newsCache: { + date: null, + content: null, + dismissed: false + }, + clientToken: null, + selectedServer: null, // Resolved + selectedAccount: null, + authenticationDatabase: {}, + modConfigurations: [] +} + +let config = null + +// Persistance Utility Functions + +/** + * Save the current configuration to a file. + */ +exports.save = function(){ + fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') +} + +/** + * Load the configuration into memory. If a configuration file exists, + * that will be read and saved. Otherwise, a default configuration will + * be generated. Note that "resolved" values default to null and will + * need to be externally assigned. + */ +exports.load = function(){ + let doLoad = true + + if(!fs.existsSync(configPath)){ + // Create all parent directories. + fs.ensureDirSync(path.join(configPath, '..')) + if(fs.existsSync(configPathLEGACY)){ + fs.moveSync(configPathLEGACY, configPath) + } else { + doLoad = false + config = DEFAULT_CONFIG + exports.save() + } + } + if(doLoad){ + let doValidate = false + try { + config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) + doValidate = true + } catch (err){ + logger.error(err) + logger.log('Configuration file contains malformed JSON or is corrupt.') + logger.log('Generating a new configuration file.') + fs.ensureDirSync(path.join(configPath, '..')) + config = DEFAULT_CONFIG + exports.save() + } + if(doValidate){ + config = validateKeySet(DEFAULT_CONFIG, config) + exports.save() + } + } + logger.log('Successfully Loaded') +} + +/** + * @returns {boolean} Whether or not the manager has been loaded. + */ +exports.isLoaded = function(){ + return config != null +} + +/** + * Validate that the destination object has at least every field + * present in the source object. Assign a default value otherwise. + * + * @param {Object} srcObj The source object to reference against. + * @param {Object} destObj The destination object. + * @returns {Object} A validated destination object. + */ +function validateKeySet(srcObj, destObj){ + if(srcObj == null){ + srcObj = {} + } + const validationBlacklist = ['authenticationDatabase'] + const keys = Object.keys(srcObj) + for(let i=0; i} An array of each stored authenticated account. + */ +exports.getAuthAccounts = function(){ + return config.authenticationDatabase +} + +/** + * Returns the authenticated account with the given uuid. Value may + * be null. + * + * @param {string} uuid The uuid of the authenticated account. + * @returns {Object} The authenticated account with the given uuid. + */ +exports.getAuthAccount = function(uuid){ + return config.authenticationDatabase[uuid] +} + +/** + * Update the access token of an authenticated account. + * + * @param {string} uuid The uuid of the authenticated account. + * @param {string} accessToken The new Access Token. + * + * @returns {Object} The authenticated account object created by this action. + */ +exports.updateAuthAccount = function(uuid, accessToken){ + config.authenticationDatabase[uuid].accessToken = accessToken + return config.authenticationDatabase[uuid] +} + +/** + * Adds an authenticated account to the database to be stored. + * + * @param {string} uuid The uuid of the authenticated account. + * @param {string} accessToken The accessToken of the authenticated account. + * @param {string} username The username (usually email) of the authenticated account. + * @param {string} displayName The in game name of the authenticated account. + * + * @returns {Object} The authenticated account object created by this action. + */ +exports.addAuthAccount = function(uuid, accessToken, username, displayName){ + config.selectedAccount = uuid + config.authenticationDatabase[uuid] = { + accessToken, + username: username.trim(), + uuid: uuid.trim(), + displayName: displayName.trim() + } + return config.authenticationDatabase[uuid] +} + +/** + * Remove an authenticated account from the database. If the account + * was also the selected account, a new one will be selected. If there + * are no accounts, the selected account will be null. + * + * @param {string} uuid The uuid of the authenticated account. + * + * @returns {boolean} True if the account was removed, false if it never existed. + */ +exports.removeAuthAccount = function(uuid){ + if(config.authenticationDatabase[uuid] != null){ + delete config.authenticationDatabase[uuid] + if(config.selectedAccount === uuid){ + const keys = Object.keys(config.authenticationDatabase) + if(keys.length > 0){ + config.selectedAccount = keys[0] + } else { + config.selectedAccount = null + config.clientToken = null + } + } + return true + } + return false +} + +/** + * Get the currently selected authenticated account. + * + * @returns {Object} The selected authenticated account. + */ +exports.getSelectedAccount = function(){ + return config.authenticationDatabase[config.selectedAccount] +} + +/** + * Set the selected authenticated account. + * + * @param {string} uuid The UUID of the account which is to be set + * as the selected account. + * + * @returns {Object} The selected authenticated account. + */ +exports.setSelectedAccount = function(uuid){ + const authAcc = config.authenticationDatabase[uuid] + if(authAcc != null) { + config.selectedAccount = uuid + } + return authAcc +} + +/** + * Get an array of each mod configuration currently stored. + * + * @returns {Array.} An array of each stored mod configuration. + */ +exports.getModConfigurations = function(){ + return config.modConfigurations +} + +/** + * Set the array of stored mod configurations. + * + * @param {Array.} configurations An array of mod configurations. + */ +exports.setModConfigurations = function(configurations){ + config.modConfigurations = configurations +} + +/** + * Get the mod configuration for a specific server. + * + * @param {string} serverid The id of the server. + * @returns {Object} The mod configuration for the given server. + */ +exports.getModConfiguration = function(serverid){ + const cfgs = config.modConfigurations + for(let i=0; i} An array of the additional arguments for JVM initialization. + */ +exports.getJVMOptions = function(def = false){ + return !def ? config.settings.java.jvmOptions : DEFAULT_CONFIG.settings.java.jvmOptions +} + +/** + * Set the additional arguments for JVM initialization. Required arguments, + * such as memory allocation, will be dynamically resolved and should not be + * included in this value. + * + * @param {Array.} jvmOptions An array of the new additional arguments for JVM + * initialization. + */ +exports.setJVMOptions = function(jvmOptions){ + config.settings.java.jvmOptions = jvmOptions +} + +// Game Settings + +/** + * Retrieve the width of the game window. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {number} The width of the game window. + */ +exports.getGameWidth = function(def = false){ + return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth +} + +/** + * Set the width of the game window. + * + * @param {number} resWidth The new width of the game window. + */ +exports.setGameWidth = function(resWidth){ + config.settings.game.resWidth = Number.parseInt(resWidth) +} + +/** + * Validate a potential new width value. + * + * @param {number} resWidth The width value to validate. + * @returns {boolean} Whether or not the value is valid. + */ +exports.validateGameWidth = function(resWidth){ + const nVal = Number.parseInt(resWidth) + return Number.isInteger(nVal) && nVal >= 0 +} + +/** + * Retrieve the height of the game window. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {number} The height of the game window. + */ +exports.getGameHeight = function(def = false){ + return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight +} + +/** + * Set the height of the game window. + * + * @param {number} resHeight The new height of the game window. + */ +exports.setGameHeight = function(resHeight){ + config.settings.game.resHeight = Number.parseInt(resHeight) +} + +/** + * Validate a potential new height value. + * + * @param {number} resHeight The height value to validate. + * @returns {boolean} Whether or not the value is valid. + */ +exports.validateGameHeight = function(resHeight){ + const nVal = Number.parseInt(resHeight) + return Number.isInteger(nVal) && nVal >= 0 +} + +/** + * Check if the game should be launched in fullscreen mode. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the game is set to launch in fullscreen mode. + */ +exports.getFullscreen = function(def = false){ + return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen +} + +/** + * Change the status of if the game should be launched in fullscreen mode. + * + * @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode. + */ +exports.setFullscreen = function(fullscreen){ + config.settings.game.fullscreen = fullscreen +} + +/** + * Check if the game should auto connect to servers. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the game should auto connect to servers. + */ +exports.getAutoConnect = function(def = false){ + return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect +} + +/** + * Change the status of whether or not the game should auto connect to servers. + * + * @param {boolean} autoConnect Whether or not the game should auto connect to servers. + */ +exports.setAutoConnect = function(autoConnect){ + config.settings.game.autoConnect = autoConnect +} + +/** + * Check if the game should launch as a detached process. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the game will launch as a detached process. + */ +exports.getLaunchDetached = function(def = false){ + return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached +} + +/** + * Change the status of whether or not the game should launch as a detached process. + * + * @param {boolean} launchDetached Whether or not the game should launch as a detached process. + */ +exports.setLaunchDetached = function(launchDetached){ + config.settings.game.launchDetached = launchDetached +} + +// Launcher Settings + +/** + * Check if the launcher should download prerelease versions. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {boolean} Whether or not the launcher should download prerelease versions. + */ +exports.getAllowPrerelease = function(def = false){ + return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease +} + +/** + * Change the status of Whether or not the launcher should download prerelease versions. + * + * @param {boolean} launchDetached Whether or not the launcher should download prerelease versions. + */ +exports.setAllowPrerelease = function(allowPrerelease){ + config.settings.launcher.allowPrerelease = allowPrerelease } \ No newline at end of file diff --git a/app/assets/js/discordwrapper.js b/app/assets/js/discordwrapper.js index 529f17be..76e81ad9 100644 --- a/app/assets/js/discordwrapper.js +++ b/app/assets/js/discordwrapper.js @@ -1,48 +1,48 @@ -// Work in progress -const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold') - -const {Client} = require('discord-rpc') - -let client -let activity - -exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){ - client = new Client({ transport: 'ipc' }) - - activity = { - details: initialDetails, - state: 'Server: ' + servSettings.shortId, - largeImageKey: servSettings.largeImageKey, - largeImageText: servSettings.largeImageText, - smallImageKey: genSettings.smallImageKey, - smallImageText: genSettings.smallImageText, - startTimestamp: new Date().getTime(), - instance: false - } - - client.on('ready', () => { - logger.log('Discord RPC Connected') - client.setActivity(activity) - }) - - client.login({clientId: genSettings.clientId}).catch(error => { - if(error.message.includes('ENOENT')) { - logger.log('Unable to initialize Discord Rich Presence, no client detected.') - } else { - logger.log('Unable to initialize Discord Rich Presence: ' + error.message, error) - } - }) -} - -exports.updateDetails = function(details){ - activity.details = details - client.setActivity(activity) -} - -exports.shutdownRPC = function(){ - if(!client) return - client.clearActivity() - client.destroy() - client = null - activity = null +// Work in progress +const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold') + +const {Client} = require('discord-rpc') + +let client +let activity + +exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){ + client = new Client({ transport: 'ipc' }) + + activity = { + details: initialDetails, + state: 'Server: ' + servSettings.shortId, + largeImageKey: servSettings.largeImageKey, + largeImageText: servSettings.largeImageText, + smallImageKey: genSettings.smallImageKey, + smallImageText: genSettings.smallImageText, + startTimestamp: new Date().getTime(), + instance: false + } + + client.on('ready', () => { + logger.log('Discord RPC Connected') + client.setActivity(activity) + }) + + client.login({clientId: genSettings.clientId}).catch(error => { + if(error.message.includes('ENOENT')) { + logger.log('Unable to initialize Discord Rich Presence, no client detected.') + } else { + logger.log('Unable to initialize Discord Rich Presence: ' + error.message, error) + } + }) +} + +exports.updateDetails = function(details){ + activity.details = details + client.setActivity(activity) +} + +exports.shutdownRPC = function(){ + if(!client) return + client.clearActivity() + client.destroy() + client = null + activity = null } \ No newline at end of file diff --git a/app/assets/js/distromanager.js b/app/assets/js/distromanager.js index a4cafffa..333109ee 100644 --- a/app/assets/js/distromanager.js +++ b/app/assets/js/distromanager.js @@ -1,605 +1,605 @@ -const fs = require('fs') -const path = require('path') -const request = require('request') - -const ConfigManager = require('./configmanager') -const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold') - -/** - * Represents the download information - * for a specific module. - */ -class Artifact { - - /** - * Parse a JSON object into an Artifact. - * - * @param {Object} json A JSON object representing an Artifact. - * - * @returns {Artifact} The parsed Artifact. - */ - static fromJSON(json){ - return Object.assign(new Artifact(), json) - } - - /** - * Get the MD5 hash of the artifact. This value may - * be undefined for artifacts which are not to be - * validated and updated. - * - * @returns {string} The MD5 hash of the Artifact or undefined. - */ - getHash(){ - return this.MD5 - } - - /** - * @returns {number} The download size of the artifact. - */ - getSize(){ - return this.size - } - - /** - * @returns {string} The download url of the artifact. - */ - getURL(){ - return this.url - } - - /** - * @returns {string} The artifact's destination path. - */ - getPath(){ - return this.path - } - -} -exports.Artifact - -/** - * Represents a the requirement status - * of a module. - */ -class Required { - - /** - * Parse a JSON object into a Required object. - * - * @param {Object} json A JSON object representing a Required object. - * - * @returns {Required} The parsed Required object. - */ - static fromJSON(json){ - if(json == null){ - return new Required(true, true) - } else { - return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def) - } - } - - constructor(value, def){ - this.value = value - this.default = def - } - - /** - * Get the default value for a required object. If a module - * is not required, this value determines whether or not - * it is enabled by default. - * - * @returns {boolean} The default enabled value. - */ - isDefault(){ - return this.default - } - - /** - * @returns {boolean} Whether or not the module is required. - */ - isRequired(){ - return this.value - } - -} -exports.Required - -/** - * Represents a module. - */ -class Module { - - /** - * Parse a JSON object into a Module. - * - * @param {Object} json A JSON object representing a Module. - * @param {string} serverid The ID of the server to which this module belongs. - * - * @returns {Module} The parsed Module. - */ - static fromJSON(json, serverid){ - return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid) - } - - /** - * Resolve the default extension for a specific module type. - * - * @param {string} type The type of the module. - * - * @return {string} The default extension for the given type. - */ - static _resolveDefaultExtension(type){ - switch (type) { - case exports.Types.Library: - case exports.Types.ForgeHosted: - case exports.Types.LiteLoader: - case exports.Types.ForgeMod: - return 'jar' - case exports.Types.LiteMod: - return 'litemod' - case exports.Types.File: - default: - return 'jar' // There is no default extension really. - } - } - - constructor(id, name, type, required, artifact, subModules, serverid) { - this.identifier = id - this.type = type - this._resolveMetaData() - this.name = name - this.required = Required.fromJSON(required) - this.artifact = Artifact.fromJSON(artifact) - this._resolveArtifactPath(artifact.path, serverid) - this._resolveSubModules(subModules, serverid) - } - - _resolveMetaData(){ - try { - - const m0 = this.identifier.split('@') - - this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type) - - const m1 = m0[0].split(':') - - this.artifactClassifier = m1[3] || undefined - this.artifactVersion = m1[2] || '???' - this.artifactID = m1[1] || '???' - this.artifactGroup = m1[0] || '???' - - } catch (err) { - // Improper identifier - logger.error('Improper ID for module', this.identifier, err) - } - } - - _resolveArtifactPath(artifactPath, serverid){ - const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath - - switch (this.type){ - case exports.Types.Library: - case exports.Types.ForgeHosted: - case exports.Types.LiteLoader: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth) - break - case exports.Types.ForgeMod: - case exports.Types.LiteMod: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth) - break - case exports.Types.VersionManifest: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`) - break - case exports.Types.File: - default: - this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth) - break - } - - } - - _resolveSubModules(json, serverid){ - const arr = [] - if(json != null){ - for(let sm of json){ - arr.push(Module.fromJSON(sm, serverid)) - } - } - this.subModules = arr.length > 0 ? arr : null - } - - /** - * @returns {string} The full, unparsed module identifier. - */ - getIdentifier(){ - return this.identifier - } - - /** - * @returns {string} The name of the module. - */ - getName(){ - return this.name - } - - /** - * @returns {Required} The required object declared by this module. - */ - getRequired(){ - return this.required - } - - /** - * @returns {Artifact} The artifact declared by this module. - */ - getArtifact(){ - return this.artifact - } - - /** - * @returns {string} The maven identifier of this module's artifact. - */ - getID(){ - return this.artifactID - } - - /** - * @returns {string} The maven group of this module's artifact. - */ - getGroup(){ - return this.artifactGroup - } - - /** - * @returns {string} The identifier without he version or extension. - */ - getVersionlessID(){ - return this.getGroup() + ':' + this.getID() - } - - /** - * @returns {string} The identifier without the extension. - */ - getExtensionlessID(){ - return this.getIdentifier().split('@')[0] - } - - /** - * @returns {string} The version of this module's artifact. - */ - getVersion(){ - return this.artifactVersion - } - - /** - * @returns {string} The classifier of this module's artifact - */ - getClassifier(){ - return this.artifactClassifier - } - - /** - * @returns {string} The extension of this module's artifact. - */ - getExtension(){ - return this.artifactExt - } - - /** - * @returns {boolean} Whether or not this module has sub modules. - */ - hasSubModules(){ - return this.subModules != null - } - - /** - * @returns {Array.} An array of sub modules. - */ - getSubModules(){ - return this.subModules - } - - /** - * @returns {string} The type of the module. - */ - getType(){ - return this.type - } - -} -exports.Module - -/** - * Represents a server configuration. - */ -class Server { - - /** - * Parse a JSON object into a Server. - * - * @param {Object} json A JSON object representing a Server. - * - * @returns {Server} The parsed Server object. - */ - static fromJSON(json){ - - const mdls = json.modules - json.modules = [] - - const serv = Object.assign(new Server(), json) - serv._resolveModules(mdls) - - return serv - } - - _resolveModules(json){ - const arr = [] - for(let m of json){ - arr.push(Module.fromJSON(m, this.getID())) - } - this.modules = arr - } - - /** - * @returns {string} The ID of the server. - */ - getID(){ - return this.id - } - - /** - * @returns {string} The name of the server. - */ - getName(){ - return this.name - } - - /** - * @returns {string} The description of the server. - */ - getDescription(){ - return this.description - } - - /** - * @returns {string} The URL of the server's icon. - */ - getIcon(){ - return this.icon - } - - /** - * @returns {string} The version of the server configuration. - */ - getVersion(){ - return this.version - } - - /** - * @returns {string} The IP address of the server. - */ - getAddress(){ - return this.address - } - - /** - * @returns {string} The minecraft version of the server. - */ - getMinecraftVersion(){ - return this.minecraftVersion - } - - /** - * @returns {boolean} Whether or not this server is the main - * server. The main server is selected by the launcher when - * no valid server is selected. - */ - isMainServer(){ - return this.mainServer - } - - /** - * @returns {boolean} Whether or not the server is autoconnect. - * by default. - */ - isAutoConnect(){ - return this.autoconnect - } - - /** - * @returns {Array.} An array of modules for this server. - */ - getModules(){ - return this.modules - } - -} -exports.Server - -/** - * Represents the Distribution Index. - */ -class DistroIndex { - - /** - * Parse a JSON object into a DistroIndex. - * - * @param {Object} json A JSON object representing a DistroIndex. - * - * @returns {DistroIndex} The parsed Server object. - */ - static fromJSON(json){ - - const servers = json.servers - json.servers = [] - - const distro = Object.assign(new DistroIndex(), json) - distro._resolveServers(servers) - distro._resolveMainServer() - - return distro - } - - _resolveServers(json){ - const arr = [] - for(let s of json){ - arr.push(Server.fromJSON(s)) - } - this.servers = arr - } - - _resolveMainServer(){ - - for(let serv of this.servers){ - if(serv.mainServer){ - this.mainServer = serv.id - return - } - } - - // If no server declares default_selected, default to the first one declared. - this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null - } - - /** - * @returns {string} The version of the distribution index. - */ - getVersion(){ - return this.version - } - - /** - * @returns {string} The URL to the news RSS feed. - */ - getRSS(){ - return this.rss - } - - /** - * @returns {Array.} An array of declared server configurations. - */ - getServers(){ - return this.servers - } - - /** - * Get a server configuration by its ID. If it does not - * exist, null will be returned. - * - * @param {string} id The ID of the server. - * - * @returns {Server} The server configuration with the given ID or null. - */ - getServer(id){ - for(let serv of this.servers){ - if(serv.id === id){ - return serv - } - } - return null - } - - /** - * Get the main server. - * - * @returns {Server} The main server. - */ - getMainServer(){ - return this.mainServer != null ? this.getServer(this.mainServer) : null - } - -} -exports.DistroIndex - -exports.Types = { - Library: 'Library', - ForgeHosted: 'ForgeHosted', - Forge: 'Forge', // Unimplemented - LiteLoader: 'LiteLoader', - ForgeMod: 'ForgeMod', - LiteMod: 'LiteMod', - File: 'File', - VersionManifest: 'VersionManifest' -} - -let DEV_MODE = false - -const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') -const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') - -let data = null - -/** - * @returns {Promise.} - */ -exports.pullRemote = function(){ - if(DEV_MODE){ - return exports.pullLocal() - } - return new Promise((resolve, reject) => { - const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json' - //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/' - const opts = { - url: distroURL, - timeout: 2500 - } - const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') - request(opts, (error, resp, body) => { - if(!error){ - - try { - data = DistroIndex.fromJSON(JSON.parse(body)) - } catch (e) { - reject(e) - } - - fs.writeFile(distroDest, body, 'utf-8', (err) => { - if(!err){ - resolve(data) - } else { - reject(err) - } - }) - } else { - reject(error) - } - }) - }) -} - -/** - * @returns {Promise.} - */ -exports.pullLocal = function(){ - return new Promise((resolve, reject) => { - fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => { - if(!err){ - data = DistroIndex.fromJSON(JSON.parse(d)) - resolve(data) - } else { - reject(err) - } - }) - }) -} - -exports.setDevMode = function(value){ - if(value){ - logger.log('Developer mode enabled.') - logger.log('If you don\'t know what that means, revert immediately.') - } else { - logger.log('Developer mode disabled.') - } - DEV_MODE = value -} - -exports.isDevMode = function(){ - return DEV_MODE -} - -/** - * @returns {DistroIndex} - */ -exports.getDistribution = function(){ - return data +const fs = require('fs') +const path = require('path') +const request = require('request') + +const ConfigManager = require('./configmanager') +const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold') + +/** + * Represents the download information + * for a specific module. + */ +class Artifact { + + /** + * Parse a JSON object into an Artifact. + * + * @param {Object} json A JSON object representing an Artifact. + * + * @returns {Artifact} The parsed Artifact. + */ + static fromJSON(json){ + return Object.assign(new Artifact(), json) + } + + /** + * Get the MD5 hash of the artifact. This value may + * be undefined for artifacts which are not to be + * validated and updated. + * + * @returns {string} The MD5 hash of the Artifact or undefined. + */ + getHash(){ + return this.MD5 + } + + /** + * @returns {number} The download size of the artifact. + */ + getSize(){ + return this.size + } + + /** + * @returns {string} The download url of the artifact. + */ + getURL(){ + return this.url + } + + /** + * @returns {string} The artifact's destination path. + */ + getPath(){ + return this.path + } + +} +exports.Artifact + +/** + * Represents a the requirement status + * of a module. + */ +class Required { + + /** + * Parse a JSON object into a Required object. + * + * @param {Object} json A JSON object representing a Required object. + * + * @returns {Required} The parsed Required object. + */ + static fromJSON(json){ + if(json == null){ + return new Required(true, true) + } else { + return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def) + } + } + + constructor(value, def){ + this.value = value + this.default = def + } + + /** + * Get the default value for a required object. If a module + * is not required, this value determines whether or not + * it is enabled by default. + * + * @returns {boolean} The default enabled value. + */ + isDefault(){ + return this.default + } + + /** + * @returns {boolean} Whether or not the module is required. + */ + isRequired(){ + return this.value + } + +} +exports.Required + +/** + * Represents a module. + */ +class Module { + + /** + * Parse a JSON object into a Module. + * + * @param {Object} json A JSON object representing a Module. + * @param {string} serverid The ID of the server to which this module belongs. + * + * @returns {Module} The parsed Module. + */ + static fromJSON(json, serverid){ + return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid) + } + + /** + * Resolve the default extension for a specific module type. + * + * @param {string} type The type of the module. + * + * @return {string} The default extension for the given type. + */ + static _resolveDefaultExtension(type){ + switch (type) { + case exports.Types.Library: + case exports.Types.ForgeHosted: + case exports.Types.LiteLoader: + case exports.Types.ForgeMod: + return 'jar' + case exports.Types.LiteMod: + return 'litemod' + case exports.Types.File: + default: + return 'jar' // There is no default extension really. + } + } + + constructor(id, name, type, required, artifact, subModules, serverid) { + this.identifier = id + this.type = type + this._resolveMetaData() + this.name = name + this.required = Required.fromJSON(required) + this.artifact = Artifact.fromJSON(artifact) + this._resolveArtifactPath(artifact.path, serverid) + this._resolveSubModules(subModules, serverid) + } + + _resolveMetaData(){ + try { + + const m0 = this.identifier.split('@') + + this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type) + + const m1 = m0[0].split(':') + + this.artifactClassifier = m1[3] || undefined + this.artifactVersion = m1[2] || '???' + this.artifactID = m1[1] || '???' + this.artifactGroup = m1[0] || '???' + + } catch (err) { + // Improper identifier + logger.error('Improper ID for module', this.identifier, err) + } + } + + _resolveArtifactPath(artifactPath, serverid){ + const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath + + switch (this.type){ + case exports.Types.Library: + case exports.Types.ForgeHosted: + case exports.Types.LiteLoader: + this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth) + break + case exports.Types.ForgeMod: + case exports.Types.LiteMod: + this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth) + break + case exports.Types.VersionManifest: + this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`) + break + case exports.Types.File: + default: + this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth) + break + } + + } + + _resolveSubModules(json, serverid){ + const arr = [] + if(json != null){ + for(let sm of json){ + arr.push(Module.fromJSON(sm, serverid)) + } + } + this.subModules = arr.length > 0 ? arr : null + } + + /** + * @returns {string} The full, unparsed module identifier. + */ + getIdentifier(){ + return this.identifier + } + + /** + * @returns {string} The name of the module. + */ + getName(){ + return this.name + } + + /** + * @returns {Required} The required object declared by this module. + */ + getRequired(){ + return this.required + } + + /** + * @returns {Artifact} The artifact declared by this module. + */ + getArtifact(){ + return this.artifact + } + + /** + * @returns {string} The maven identifier of this module's artifact. + */ + getID(){ + return this.artifactID + } + + /** + * @returns {string} The maven group of this module's artifact. + */ + getGroup(){ + return this.artifactGroup + } + + /** + * @returns {string} The identifier without he version or extension. + */ + getVersionlessID(){ + return this.getGroup() + ':' + this.getID() + } + + /** + * @returns {string} The identifier without the extension. + */ + getExtensionlessID(){ + return this.getIdentifier().split('@')[0] + } + + /** + * @returns {string} The version of this module's artifact. + */ + getVersion(){ + return this.artifactVersion + } + + /** + * @returns {string} The classifier of this module's artifact + */ + getClassifier(){ + return this.artifactClassifier + } + + /** + * @returns {string} The extension of this module's artifact. + */ + getExtension(){ + return this.artifactExt + } + + /** + * @returns {boolean} Whether or not this module has sub modules. + */ + hasSubModules(){ + return this.subModules != null + } + + /** + * @returns {Array.} An array of sub modules. + */ + getSubModules(){ + return this.subModules + } + + /** + * @returns {string} The type of the module. + */ + getType(){ + return this.type + } + +} +exports.Module + +/** + * Represents a server configuration. + */ +class Server { + + /** + * Parse a JSON object into a Server. + * + * @param {Object} json A JSON object representing a Server. + * + * @returns {Server} The parsed Server object. + */ + static fromJSON(json){ + + const mdls = json.modules + json.modules = [] + + const serv = Object.assign(new Server(), json) + serv._resolveModules(mdls) + + return serv + } + + _resolveModules(json){ + const arr = [] + for(let m of json){ + arr.push(Module.fromJSON(m, this.getID())) + } + this.modules = arr + } + + /** + * @returns {string} The ID of the server. + */ + getID(){ + return this.id + } + + /** + * @returns {string} The name of the server. + */ + getName(){ + return this.name + } + + /** + * @returns {string} The description of the server. + */ + getDescription(){ + return this.description + } + + /** + * @returns {string} The URL of the server's icon. + */ + getIcon(){ + return this.icon + } + + /** + * @returns {string} The version of the server configuration. + */ + getVersion(){ + return this.version + } + + /** + * @returns {string} The IP address of the server. + */ + getAddress(){ + return this.address + } + + /** + * @returns {string} The minecraft version of the server. + */ + getMinecraftVersion(){ + return this.minecraftVersion + } + + /** + * @returns {boolean} Whether or not this server is the main + * server. The main server is selected by the launcher when + * no valid server is selected. + */ + isMainServer(){ + return this.mainServer + } + + /** + * @returns {boolean} Whether or not the server is autoconnect. + * by default. + */ + isAutoConnect(){ + return this.autoconnect + } + + /** + * @returns {Array.} An array of modules for this server. + */ + getModules(){ + return this.modules + } + +} +exports.Server + +/** + * Represents the Distribution Index. + */ +class DistroIndex { + + /** + * Parse a JSON object into a DistroIndex. + * + * @param {Object} json A JSON object representing a DistroIndex. + * + * @returns {DistroIndex} The parsed Server object. + */ + static fromJSON(json){ + + const servers = json.servers + json.servers = [] + + const distro = Object.assign(new DistroIndex(), json) + distro._resolveServers(servers) + distro._resolveMainServer() + + return distro + } + + _resolveServers(json){ + const arr = [] + for(let s of json){ + arr.push(Server.fromJSON(s)) + } + this.servers = arr + } + + _resolveMainServer(){ + + for(let serv of this.servers){ + if(serv.mainServer){ + this.mainServer = serv.id + return + } + } + + // If no server declares default_selected, default to the first one declared. + this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null + } + + /** + * @returns {string} The version of the distribution index. + */ + getVersion(){ + return this.version + } + + /** + * @returns {string} The URL to the news RSS feed. + */ + getRSS(){ + return this.rss + } + + /** + * @returns {Array.} An array of declared server configurations. + */ + getServers(){ + return this.servers + } + + /** + * Get a server configuration by its ID. If it does not + * exist, null will be returned. + * + * @param {string} id The ID of the server. + * + * @returns {Server} The server configuration with the given ID or null. + */ + getServer(id){ + for(let serv of this.servers){ + if(serv.id === id){ + return serv + } + } + return null + } + + /** + * Get the main server. + * + * @returns {Server} The main server. + */ + getMainServer(){ + return this.mainServer != null ? this.getServer(this.mainServer) : null + } + +} +exports.DistroIndex + +exports.Types = { + Library: 'Library', + ForgeHosted: 'ForgeHosted', + Forge: 'Forge', // Unimplemented + LiteLoader: 'LiteLoader', + ForgeMod: 'ForgeMod', + LiteMod: 'LiteMod', + File: 'File', + VersionManifest: 'VersionManifest' +} + +let DEV_MODE = false + +const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') +const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') + +let data = null + +/** + * @returns {Promise.} + */ +exports.pullRemote = function(){ + if(DEV_MODE){ + return exports.pullLocal() + } + return new Promise((resolve, reject) => { + const distroURL = 'https://raw.githubusercontent.com/MastermDEV/NemesisMC-Launcher/master/app/assets/distribution.json' + //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/' + const opts = { + url: distroURL, + timeout: 2500 + } + const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') + request(opts, (error, resp, body) => { + if(!error){ + + try { + data = DistroIndex.fromJSON(JSON.parse(body)) + } catch (e) { + reject(e) + } + + fs.writeFile(distroDest, body, 'utf-8', (err) => { + if(!err){ + resolve(data) + } else { + reject(err) + } + }) + } else { + reject(error) + } + }) + }) +} + +/** + * @returns {Promise.} + */ +exports.pullLocal = function(){ + return new Promise((resolve, reject) => { + fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => { + if(!err){ + data = DistroIndex.fromJSON(JSON.parse(d)) + resolve(data) + } else { + reject(err) + } + }) + }) +} + +exports.setDevMode = function(value){ + if(value){ + logger.log('Developer mode enabled.') + logger.log('If you don\'t know what that means, revert immediately.') + } else { + logger.log('Developer mode disabled.') + } + DEV_MODE = value +} + +exports.isDevMode = function(){ + return DEV_MODE +} + +/** + * @returns {DistroIndex} + */ +exports.getDistribution = function(){ + return data } \ No newline at end of file diff --git a/app/assets/js/dropinmodutil.js b/app/assets/js/dropinmodutil.js index 0a61012e..10e5ad3d 100644 --- a/app/assets/js/dropinmodutil.js +++ b/app/assets/js/dropinmodutil.js @@ -1,232 +1,232 @@ -const fs = require('fs-extra') -const path = require('path') -const { shell } = require('electron') - -// Group #1: File Name (without .disabled, if any) -// Group #2: File Extension (jar, zip, or litemod) -// Group #3: If it is disabled (if string 'disabled' is present) -const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ -const DISABLED_EXT = '.disabled' - -const SHADER_REGEX = /^(.+)\.zip$/ -const SHADER_OPTION = /shaderPack=(.+)/ -const SHADER_DIR = 'shaderpacks' -const SHADER_CONFIG = 'optionsshaders.txt' - -/** - * Validate that the given directory exists. If not, it is - * created. - * - * @param {string} modsDir The path to the mods directory. - */ -exports.validateDir = function(dir) { - fs.ensureDirSync(dir) -} - -/** - * Scan for drop-in mods in both the mods folder and version - * safe mods folder. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} version The minecraft version of the server configuration. - * - * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} - * An array of objects storing metadata about each discovered mod. - */ -exports.scanForDropinMods = function(modsDir, version) { - const modsDiscovered = [] - if(fs.existsSync(modsDir)){ - let modCandidates = fs.readdirSync(modsDir) - let verCandidates = [] - const versionDir = path.join(modsDir, version) - if(fs.existsSync(versionDir)){ - verCandidates = fs.readdirSync(versionDir) - } - for(let file of modCandidates){ - const match = MOD_REGEX.exec(file) - if(match != null){ - modsDiscovered.push({ - fullName: match[0], - name: match[1], - ext: match[2], - disabled: match[3] != null - }) - } - } - for(let file of verCandidates){ - const match = MOD_REGEX.exec(file) - if(match != null){ - modsDiscovered.push({ - fullName: path.join(version, match[0]), - name: match[1], - ext: match[2], - disabled: match[3] != null - }) - } - } - } - return modsDiscovered -} - -/** - * Add dropin mods. - * - * @param {FileList} files The files to add. - * @param {string} modsDir The path to the mods directory. - */ -exports.addDropinMods = function(files, modsdir) { - - exports.validateDir(modsdir) - - for(let f of files) { - if(MOD_REGEX.exec(f.name) != null) { - fs.moveSync(f.path, path.join(modsdir, f.name)) - } - } - -} - -/** - * Delete a drop-in mod from the file system. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} fullName The fullName of the discovered mod to delete. - * - * @returns {boolean} True if the mod was deleted, otherwise false. - */ -exports.deleteDropinMod = function(modsDir, fullName){ - const res = shell.moveItemToTrash(path.join(modsDir, fullName)) - if(!res){ - shell.beep() - } - return res -} - -/** - * Toggle a discovered mod on or off. This is achieved by either - * adding or disabling the .disabled extension to the local file. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} fullName The fullName of the discovered mod to toggle. - * @param {boolean} enable Whether to toggle on or off the mod. - * - * @returns {Promise.} A promise which resolves when the mod has - * been toggled. If an IO error occurs the promise will be rejected. - */ -exports.toggleDropinMod = function(modsDir, fullName, enable){ - return new Promise((resolve, reject) => { - const oldPath = path.join(modsDir, fullName) - const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) - - fs.rename(oldPath, newPath, (err) => { - if(err){ - reject(err) - } else { - resolve() - } - }) - }) -} - -/** - * Check if a drop-in mod is enabled. - * - * @param {string} fullName The fullName of the discovered mod to toggle. - * @returns {boolean} True if the mod is enabled, otherwise false. - */ -exports.isDropinModEnabled = function(fullName){ - return !fullName.endsWith(DISABLED_EXT) -} - -/** - * Scan for shaderpacks inside the shaderpacks folder. - * - * @param {string} instanceDir The path to the server instance directory. - * - * @returns {{fullName: string, name: string}[]} - * An array of objects storing metadata about each discovered shaderpack. - */ -exports.scanForShaderpacks = function(instanceDir){ - const shaderDir = path.join(instanceDir, SHADER_DIR) - const packsDiscovered = [{ - fullName: 'OFF', - name: 'Off (Default)' - }] - if(fs.existsSync(shaderDir)){ - let modCandidates = fs.readdirSync(shaderDir) - for(let file of modCandidates){ - const match = SHADER_REGEX.exec(file) - if(match != null){ - packsDiscovered.push({ - fullName: match[0], - name: match[1] - }) - } - } - } - return packsDiscovered -} - -/** - * Read the optionsshaders.txt file to locate the current - * enabled pack. If the file does not exist, OFF is returned. - * - * @param {string} instanceDir The path to the server instance directory. - * - * @returns {string} The file name of the enabled shaderpack. - */ -exports.getEnabledShaderpack = function(instanceDir){ - exports.validateDir(instanceDir) - - const optionsShaders = path.join(instanceDir, SHADER_CONFIG) - if(fs.existsSync(optionsShaders)){ - const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) - const match = SHADER_OPTION.exec(buf) - if(match != null){ - return match[1] - } else { - console.warn('WARNING: Shaderpack regex failed.') - } - } - return 'OFF' -} - -/** - * Set the enabled shaderpack. - * - * @param {string} instanceDir The path to the server instance directory. - * @param {string} pack the file name of the shaderpack. - */ -exports.setEnabledShaderpack = function(instanceDir, pack){ - exports.validateDir(instanceDir) - - const optionsShaders = path.join(instanceDir, SHADER_CONFIG) - let buf - if(fs.existsSync(optionsShaders)){ - buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) - buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`) - } else { - buf = `shaderPack=${pack}` - } - fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'}) -} - -/** - * Add shaderpacks. - * - * @param {FileList} files The files to add. - * @param {string} instanceDir The path to the server instance directory. - */ -exports.addShaderpacks = function(files, instanceDir) { - - const p = path.join(instanceDir, SHADER_DIR) - - exports.validateDir(p) - - for(let f of files) { - if(SHADER_REGEX.exec(f.name) != null) { - fs.moveSync(f.path, path.join(p, f.name)) - } - } - +const fs = require('fs-extra') +const path = require('path') +const { shell } = require('electron') + +// Group #1: File Name (without .disabled, if any) +// Group #2: File Extension (jar, zip, or litemod) +// Group #3: If it is disabled (if string 'disabled' is present) +const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ +const DISABLED_EXT = '.disabled' + +const SHADER_REGEX = /^(.+)\.zip$/ +const SHADER_OPTION = /shaderPack=(.+)/ +const SHADER_DIR = 'shaderpacks' +const SHADER_CONFIG = 'optionsshaders.txt' + +/** + * Validate that the given directory exists. If not, it is + * created. + * + * @param {string} modsDir The path to the mods directory. + */ +exports.validateDir = function(dir) { + fs.ensureDirSync(dir) +} + +/** + * Scan for drop-in mods in both the mods folder and version + * safe mods folder. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} version The minecraft version of the server configuration. + * + * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} + * An array of objects storing metadata about each discovered mod. + */ +exports.scanForDropinMods = function(modsDir, version) { + const modsDiscovered = [] + if(fs.existsSync(modsDir)){ + let modCandidates = fs.readdirSync(modsDir) + let verCandidates = [] + const versionDir = path.join(modsDir, version) + if(fs.existsSync(versionDir)){ + verCandidates = fs.readdirSync(versionDir) + } + for(let file of modCandidates){ + const match = MOD_REGEX.exec(file) + if(match != null){ + modsDiscovered.push({ + fullName: match[0], + name: match[1], + ext: match[2], + disabled: match[3] != null + }) + } + } + for(let file of verCandidates){ + const match = MOD_REGEX.exec(file) + if(match != null){ + modsDiscovered.push({ + fullName: path.join(version, match[0]), + name: match[1], + ext: match[2], + disabled: match[3] != null + }) + } + } + } + return modsDiscovered +} + +/** + * Add dropin mods. + * + * @param {FileList} files The files to add. + * @param {string} modsDir The path to the mods directory. + */ +exports.addDropinMods = function(files, modsdir) { + + exports.validateDir(modsdir) + + for(let f of files) { + if(MOD_REGEX.exec(f.name) != null) { + fs.moveSync(f.path, path.join(modsdir, f.name)) + } + } + +} + +/** + * Delete a drop-in mod from the file system. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} fullName The fullName of the discovered mod to delete. + * + * @returns {boolean} True if the mod was deleted, otherwise false. + */ +exports.deleteDropinMod = function(modsDir, fullName){ + const res = shell.moveItemToTrash(path.join(modsDir, fullName)) + if(!res){ + shell.beep() + } + return res +} + +/** + * Toggle a discovered mod on or off. This is achieved by either + * adding or disabling the .disabled extension to the local file. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} fullName The fullName of the discovered mod to toggle. + * @param {boolean} enable Whether to toggle on or off the mod. + * + * @returns {Promise.} A promise which resolves when the mod has + * been toggled. If an IO error occurs the promise will be rejected. + */ +exports.toggleDropinMod = function(modsDir, fullName, enable){ + return new Promise((resolve, reject) => { + const oldPath = path.join(modsDir, fullName) + const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) + + fs.rename(oldPath, newPath, (err) => { + if(err){ + reject(err) + } else { + resolve() + } + }) + }) +} + +/** + * Check if a drop-in mod is enabled. + * + * @param {string} fullName The fullName of the discovered mod to toggle. + * @returns {boolean} True if the mod is enabled, otherwise false. + */ +exports.isDropinModEnabled = function(fullName){ + return !fullName.endsWith(DISABLED_EXT) +} + +/** + * Scan for shaderpacks inside the shaderpacks folder. + * + * @param {string} instanceDir The path to the server instance directory. + * + * @returns {{fullName: string, name: string}[]} + * An array of objects storing metadata about each discovered shaderpack. + */ +exports.scanForShaderpacks = function(instanceDir){ + const shaderDir = path.join(instanceDir, SHADER_DIR) + const packsDiscovered = [{ + fullName: 'OFF', + name: 'Off (Default)' + }] + if(fs.existsSync(shaderDir)){ + let modCandidates = fs.readdirSync(shaderDir) + for(let file of modCandidates){ + const match = SHADER_REGEX.exec(file) + if(match != null){ + packsDiscovered.push({ + fullName: match[0], + name: match[1] + }) + } + } + } + return packsDiscovered +} + +/** + * Read the optionsshaders.txt file to locate the current + * enabled pack. If the file does not exist, OFF is returned. + * + * @param {string} instanceDir The path to the server instance directory. + * + * @returns {string} The file name of the enabled shaderpack. + */ +exports.getEnabledShaderpack = function(instanceDir){ + exports.validateDir(instanceDir) + + const optionsShaders = path.join(instanceDir, SHADER_CONFIG) + if(fs.existsSync(optionsShaders)){ + const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) + const match = SHADER_OPTION.exec(buf) + if(match != null){ + return match[1] + } else { + console.warn('WARNING: Shaderpack regex failed.') + } + } + return 'OFF' +} + +/** + * Set the enabled shaderpack. + * + * @param {string} instanceDir The path to the server instance directory. + * @param {string} pack the file name of the shaderpack. + */ +exports.setEnabledShaderpack = function(instanceDir, pack){ + exports.validateDir(instanceDir) + + const optionsShaders = path.join(instanceDir, SHADER_CONFIG) + let buf + if(fs.existsSync(optionsShaders)){ + buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) + buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`) + } else { + buf = `shaderPack=${pack}` + } + fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'}) +} + +/** + * Add shaderpacks. + * + * @param {FileList} files The files to add. + * @param {string} instanceDir The path to the server instance directory. + */ +exports.addShaderpacks = function(files, instanceDir) { + + const p = path.join(instanceDir, SHADER_DIR) + + exports.validateDir(p) + + for(let f of files) { + if(SHADER_REGEX.exec(f.name) != null) { + fs.moveSync(f.path, path.join(p, f.name)) + } + } + } \ No newline at end of file diff --git a/app/assets/js/isdev.js b/app/assets/js/isdev.js index 1ed55e5b..e1135334 100644 --- a/app/assets/js/isdev.js +++ b/app/assets/js/isdev.js @@ -1,5 +1,5 @@ -'use strict' -const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 -const isEnvSet = 'ELECTRON_IS_DEV' in process.env - +'use strict' +const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 +const isEnvSet = 'ELECTRON_IS_DEV' in process.env + module.exports = isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)) \ No newline at end of file diff --git a/app/assets/js/langloader.js b/app/assets/js/langloader.js index 24ab84ae..3d53fb7b 100644 --- a/app/assets/js/langloader.js +++ b/app/assets/js/langloader.js @@ -1,21 +1,21 @@ -const fs = require('fs-extra') -const path = require('path') - -let lang - -exports.loadLanguage = function(id){ - lang = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.json`))) || {} -} - -exports.query = function(id){ - let query = id.split('.') - let res = lang - for(let q of query){ - res = res[q] - } - return res === lang ? {} : res -} - -exports.queryJS = function(id){ - return exports.query(`js.${id}`) +const fs = require('fs-extra') +const path = require('path') + +let lang + +exports.loadLanguage = function(id){ + lang = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.json`))) || {} +} + +exports.query = function(id){ + let query = id.split('.') + let res = lang + for(let q of query){ + res = res[q] + } + return res === lang ? {} : res +} + +exports.queryJS = function(id){ + return exports.query(`js.${id}`) } \ No newline at end of file diff --git a/app/assets/js/loggerutil.js b/app/assets/js/loggerutil.js index 73899418..014ae6c7 100644 --- a/app/assets/js/loggerutil.js +++ b/app/assets/js/loggerutil.js @@ -1,32 +1,32 @@ -class LoggerUtil { - - constructor(prefix, style){ - this.prefix = prefix - this.style = style - } - - log(){ - console.log.apply(null, [this.prefix, this.style, ...arguments]) - } - - info(){ - console.info.apply(null, [this.prefix, this.style, ...arguments]) - } - - warn(){ - console.warn.apply(null, [this.prefix, this.style, ...arguments]) - } - - debug(){ - console.debug.apply(null, [this.prefix, this.style, ...arguments]) - } - - error(){ - console.error.apply(null, [this.prefix, this.style, ...arguments]) - } - -} - -module.exports = function (prefix, style){ - return new LoggerUtil(prefix, style) +class LoggerUtil { + + constructor(prefix, style){ + this.prefix = prefix + this.style = style + } + + log(){ + console.log.apply(null, [this.prefix, this.style, ...arguments]) + } + + info(){ + console.info.apply(null, [this.prefix, this.style, ...arguments]) + } + + warn(){ + console.warn.apply(null, [this.prefix, this.style, ...arguments]) + } + + debug(){ + console.debug.apply(null, [this.prefix, this.style, ...arguments]) + } + + error(){ + console.error.apply(null, [this.prefix, this.style, ...arguments]) + } + +} + +module.exports = function (prefix, style){ + return new LoggerUtil(prefix, style) } \ No newline at end of file diff --git a/app/assets/js/mojang.js b/app/assets/js/mojang.js index 75143836..16bc729b 100644 --- a/app/assets/js/mojang.js +++ b/app/assets/js/mojang.js @@ -1,271 +1,271 @@ -/** - * Mojang - * - * This module serves as a minimal wrapper for Mojang's REST api. - * - * @module mojang - */ -// Requirements -const request = require('request') -const logger = require('./loggerutil')('%c[Mojang]', 'color: #a02d2a; font-weight: bold') - -// Constants -const minecraftAgent = { - name: 'Minecraft', - version: 1 -} -const authpath = 'https://authserver.mojang.com' -const statuses = [ - { - service: 'sessionserver.mojang.com', - status: 'grey', - name: 'Multiplayer Session Service', - essential: true - }, - { - service: 'authserver.mojang.com', - status: 'grey', - name: 'Authentication Service', - essential: true - }, - { - service: 'textures.minecraft.net', - status: 'grey', - name: 'Minecraft Skins', - essential: false - }, - { - service: 'api.mojang.com', - status: 'grey', - name: 'Public API', - essential: false - }, - { - service: 'minecraft.net', - status: 'grey', - name: 'Minecraft.net', - essential: false - }, - { - service: 'account.mojang.com', - status: 'grey', - name: 'Mojang Accounts Website', - essential: false - } -] - -// Functions - -/** - * Converts a Mojang status color to a hex value. Valid statuses - * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status - * to our project which represents an unknown status. - * - * @param {string} status A valid status code. - * @returns {string} The hex color of the status code. - */ -exports.statusToHex = function(status){ - switch(status.toLowerCase()){ - case 'green': - return '#a5c325' - case 'yellow': - return '#eac918' - case 'red': - return '#c32625' - case 'grey': - default: - return '#848484' - } -} - -/** - * Retrieves the status of Mojang's services. - * The response is condensed into a single object. Each service is - * a key, where the value is an object containing a status and name - * property. - * - * @see http://wiki.vg/Mojang_API#API_Status - */ -exports.status = function(){ - return new Promise((resolve, reject) => { - request.get('https://status.mojang.com/check', - { - json: true, - timeout: 2500 - }, - function(error, response, body){ - - if(error || response.statusCode !== 200){ - logger.warn('Unable to retrieve Mojang status.') - logger.debug('Error while retrieving Mojang statuses:', error) - //reject(error || response.statusCode) - for(let i=0; i { - - const body = { - agent, - username, - password, - requestUser - } - if(clientToken != null){ - body.clientToken = clientToken - } - - request.post(authpath + '/authenticate', - { - json: true, - body - }, - function(error, response, body){ - if(error){ - logger.error('Error during authentication.', error) - reject(error) - } else { - if(response.statusCode === 200){ - resolve(body) - } else { - reject(body || {code: 'ENOTFOUND'}) - } - } - }) - }) -} - -/** - * Validate an access token. This should always be done before launching. - * The client token should match the one used to create the access token. - * - * @param {string} accessToken The access token to validate. - * @param {string} clientToken The launcher's client token. - * - * @see http://wiki.vg/Authentication#Validate - */ -exports.validate = function(accessToken, clientToken){ - return new Promise((resolve, reject) => { - request.post(authpath + '/validate', - { - json: true, - body: { - accessToken, - clientToken - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during validation.', error) - reject(error) - } else { - if(response.statusCode === 403){ - resolve(false) - } else { - // 204 if valid - resolve(true) - } - } - }) - }) -} - -/** - * Invalidates an access token. The clientToken must match the - * token used to create the provided accessToken. - * - * @param {string} accessToken The access token to invalidate. - * @param {string} clientToken The launcher's client token. - * - * @see http://wiki.vg/Authentication#Invalidate - */ -exports.invalidate = function(accessToken, clientToken){ - return new Promise((resolve, reject) => { - request.post(authpath + '/invalidate', - { - json: true, - body: { - accessToken, - clientToken - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during invalidation.', error) - reject(error) - } else { - if(response.statusCode === 204){ - resolve() - } else { - reject(body) - } - } - }) - }) -} - -/** - * Refresh a user's authentication. This should be used to keep a user logged - * in without asking them for their credentials again. A new access token will - * be generated using a recent invalid access token. See Wiki for more info. - * - * @param {string} accessToken The old access token. - * @param {string} clientToken The launcher's client token. - * @param {boolean} requestUser Optional. Adds user object to the reponse. - * - * @see http://wiki.vg/Authentication#Refresh - */ -exports.refresh = function(accessToken, clientToken, requestUser = true){ - return new Promise((resolve, reject) => { - request.post(authpath + '/refresh', - { - json: true, - body: { - accessToken, - clientToken, - requestUser - } - }, - function(error, response, body){ - if(error){ - logger.error('Error during refresh.', error) - reject(error) - } else { - if(response.statusCode === 200){ - resolve(body) - } else { - reject(body) - } - } - }) - }) +/** + * Mojang + * + * This module serves as a minimal wrapper for Mojang's REST api. + * + * @module mojang + */ +// Requirements +const request = require('request') +const logger = require('./loggerutil')('%c[Mojang]', 'color: #a02d2a; font-weight: bold') + +// Constants +const minecraftAgent = { + name: 'Minecraft', + version: 1 +} +const authpath = 'https://authserver.mojang.com' +const statuses = [ + { + service: 'sessionserver.mojang.com', + status: 'grey', + name: 'Multiplayer Session Service', + essential: true + }, + { + service: 'authserver.mojang.com', + status: 'grey', + name: 'Authentication Service', + essential: true + }, + { + service: 'textures.minecraft.net', + status: 'grey', + name: 'Minecraft Skins', + essential: false + }, + { + service: 'api.mojang.com', + status: 'grey', + name: 'Public API', + essential: false + }, + { + service: 'minecraft.net', + status: 'grey', + name: 'Minecraft.net', + essential: false + }, + { + service: 'account.mojang.com', + status: 'grey', + name: 'Mojang Accounts Website', + essential: false + } +] + +// Functions + +/** + * Converts a Mojang status color to a hex value. Valid statuses + * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status + * to our project which represents an unknown status. + * + * @param {string} status A valid status code. + * @returns {string} The hex color of the status code. + */ +exports.statusToHex = function(status){ + switch(status.toLowerCase()){ + case 'green': + return '#a5c325' + case 'yellow': + return '#eac918' + case 'red': + return '#c32625' + case 'grey': + default: + return '#848484' + } +} + +/** + * Retrieves the status of Mojang's services. + * The response is condensed into a single object. Each service is + * a key, where the value is an object containing a status and name + * property. + * + * @see http://wiki.vg/Mojang_API#API_Status + */ +exports.status = function(){ + return new Promise((resolve, reject) => { + request.get('https://status.mojang.com/check', + { + json: true, + timeout: 2500 + }, + function(error, response, body){ + + if(error || response.statusCode !== 200){ + logger.warn('Unable to retrieve Mojang status.') + logger.debug('Error while retrieving Mojang statuses:', error) + //reject(error || response.statusCode) + for(let i=0; i { + + const body = { + agent, + username, + password, + requestUser + } + if(clientToken != null){ + body.clientToken = clientToken + } + + request.post(authpath + '/authenticate', + { + json: true, + body + }, + function(error, response, body){ + if(error){ + logger.error('Error during authentication.', error) + reject(error) + } else { + if(response.statusCode === 200){ + resolve(body) + } else { + reject(body || {code: 'ENOTFOUND'}) + } + } + }) + }) +} + +/** + * Validate an access token. This should always be done before launching. + * The client token should match the one used to create the access token. + * + * @param {string} accessToken The access token to validate. + * @param {string} clientToken The launcher's client token. + * + * @see http://wiki.vg/Authentication#Validate + */ +exports.validate = function(accessToken, clientToken){ + return new Promise((resolve, reject) => { + request.post(authpath + '/validate', + { + json: true, + body: { + accessToken, + clientToken + } + }, + function(error, response, body){ + if(error){ + logger.error('Error during validation.', error) + reject(error) + } else { + if(response.statusCode === 403){ + resolve(false) + } else { + // 204 if valid + resolve(true) + } + } + }) + }) +} + +/** + * Invalidates an access token. The clientToken must match the + * token used to create the provided accessToken. + * + * @param {string} accessToken The access token to invalidate. + * @param {string} clientToken The launcher's client token. + * + * @see http://wiki.vg/Authentication#Invalidate + */ +exports.invalidate = function(accessToken, clientToken){ + return new Promise((resolve, reject) => { + request.post(authpath + '/invalidate', + { + json: true, + body: { + accessToken, + clientToken + } + }, + function(error, response, body){ + if(error){ + logger.error('Error during invalidation.', error) + reject(error) + } else { + if(response.statusCode === 204){ + resolve() + } else { + reject(body) + } + } + }) + }) +} + +/** + * Refresh a user's authentication. This should be used to keep a user logged + * in without asking them for their credentials again. A new access token will + * be generated using a recent invalid access token. See Wiki for more info. + * + * @param {string} accessToken The old access token. + * @param {string} clientToken The launcher's client token. + * @param {boolean} requestUser Optional. Adds user object to the reponse. + * + * @see http://wiki.vg/Authentication#Refresh + */ +exports.refresh = function(accessToken, clientToken, requestUser = true){ + return new Promise((resolve, reject) => { + request.post(authpath + '/refresh', + { + json: true, + body: { + accessToken, + clientToken, + requestUser + } + }, + function(error, response, body){ + if(error){ + logger.error('Error during refresh.', error) + reject(error) + } else { + if(response.statusCode === 200){ + resolve(body) + } else { + reject(body) + } + } + }) + }) } \ No newline at end of file diff --git a/app/assets/js/preloader.js b/app/assets/js/preloader.js index 792c5304..20781d0b 100644 --- a/app/assets/js/preloader.js +++ b/app/assets/js/preloader.js @@ -1,69 +1,69 @@ -const {ipcRenderer} = require('electron') -const fs = require('fs-extra') -const os = require('os') -const path = require('path') - -const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') -const LangLoader = require('./langloader') -const logger = require('./loggerutil')('%c[Preloader]', 'color: #a02d2a; font-weight: bold') - -logger.log('Loading..') - -// Load ConfigManager -ConfigManager.load() - -// Load Strings -LangLoader.loadLanguage('en_US') - -function onDistroLoad(data){ - if(data != null){ - - // Resolve the selected server if its value has yet to be set. - if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){ - logger.log('Determining default selected server..') - ConfigManager.setSelectedServer(data.getMainServer().getID()) - ConfigManager.save() - } - } - ipcRenderer.send('distributionIndexDone', data != null) -} - -// Ensure Distribution is downloaded and cached. -DistroManager.pullRemote().then((data) => { - logger.log('Loaded distribution index.') - - onDistroLoad(data) - -}).catch((err) => { - logger.log('Failed to load distribution index.') - logger.error(err) - - logger.log('Attempting to load an older version of the distribution index.') - // Try getting a local copy, better than nothing. - DistroManager.pullLocal().then((data) => { - logger.log('Successfully loaded an older version of the distribution index.') - - onDistroLoad(data) - - - }).catch((err) => { - - logger.log('Failed to load an older version of the distribution index.') - logger.log('Application cannot run.') - logger.error(err) - - onDistroLoad(null) - - }) - -}) - -// Clean up temp dir incase previous launches ended unexpectedly. -fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { - if(err){ - logger.warn('Error while cleaning natives directory', err) - } else { - logger.log('Cleaned natives directory.') - } +const {ipcRenderer} = require('electron') +const fs = require('fs-extra') +const os = require('os') +const path = require('path') + +const ConfigManager = require('./configmanager') +const DistroManager = require('./distromanager') +const LangLoader = require('./langloader') +const logger = require('./loggerutil')('%c[Preloader]', 'color: #a02d2a; font-weight: bold') + +logger.log('Loading..') + +// Load ConfigManager +ConfigManager.load() + +// Load Strings +LangLoader.loadLanguage('en_US') + +function onDistroLoad(data){ + if(data != null){ + + // Resolve the selected server if its value has yet to be set. + if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){ + logger.log('Determining default selected server..') + ConfigManager.setSelectedServer(data.getMainServer().getID()) + ConfigManager.save() + } + } + ipcRenderer.send('distributionIndexDone', data != null) +} + +// Ensure Distribution is downloaded and cached. +DistroManager.pullRemote().then((data) => { + logger.log('Loaded distribution index.') + + onDistroLoad(data) + +}).catch((err) => { + logger.log('Failed to load distribution index.') + logger.error(err) + + logger.log('Attempting to load an older version of the distribution index.') + // Try getting a local copy, better than nothing. + DistroManager.pullLocal().then((data) => { + logger.log('Successfully loaded an older version of the distribution index.') + + onDistroLoad(data) + + + }).catch((err) => { + + logger.log('Failed to load an older version of the distribution index.') + logger.log('Application cannot run.') + logger.error(err) + + onDistroLoad(null) + + }) + +}) + +// Clean up temp dir incase previous launches ended unexpectedly. +fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { + if(err){ + logger.warn('Error while cleaning natives directory', err) + } else { + logger.log('Cleaned natives directory.') + } }) \ No newline at end of file diff --git a/app/assets/js/processbuilder.js b/app/assets/js/processbuilder.js index 060b2a97..af8c3eff 100644 --- a/app/assets/js/processbuilder.js +++ b/app/assets/js/processbuilder.js @@ -317,7 +317,7 @@ class ProcessBuilder { // Java Arguments if(process.platform === 'darwin'){ - args.push('-Xdock:name=HeliosLauncher') + args.push('-Xdock:name=NemesisMCLauncher') args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns')) } args.push('-Xmx' + ConfigManager.getMaxRAM()) @@ -355,7 +355,7 @@ class ProcessBuilder { // Java Arguments if(process.platform === 'darwin'){ - args.push('-Xdock:name=HeliosLauncher') + args.push('-Xdock:name=NemesisMCLauncher') args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns')) } args.push('-Xmx' + ConfigManager.getMaxRAM()) @@ -457,7 +457,7 @@ class ProcessBuilder { val = args[i].replace(argDiscovery, tempNativePath) break case 'launcher_name': - val = args[i].replace(argDiscovery, 'Helios-Launcher') + val = args[i].replace(argDiscovery, 'NemesisMC-Launcher') break case 'launcher_version': val = args[i].replace(argDiscovery, this.launcherVersion) diff --git a/app/assets/js/serverstatus.js b/app/assets/js/serverstatus.js index 9729f9c7..bf4e1c44 100644 --- a/app/assets/js/serverstatus.js +++ b/app/assets/js/serverstatus.js @@ -1,65 +1,65 @@ -const net = require('net') - -/** - * Retrieves the status of a minecraft server. - * - * @param {string} address The server address. - * @param {number} port Optional. The port of the server. Defaults to 25565. - * @returns {Promise.} A promise which resolves to an object containing - * status information. - */ -exports.getStatus = function(address, port = 25565){ - - if(port == null || port == ''){ - port = 25565 - } - if(typeof port === 'string'){ - port = parseInt(port) - } - - return new Promise((resolve, reject) => { - const socket = net.connect(port, address, () => { - let buff = Buffer.from([0xFE, 0x01]) - socket.write(buff) - }) - - socket.setTimeout(2500, () => { - socket.end() - reject({ - code: 'ETIMEDOUT', - errno: 'ETIMEDOUT', - address, - port - }) - }) - - socket.on('data', (data) => { - if(data != null && data != ''){ - let server_info = data.toString().split('\x00\x00\x00') - const NUM_FIELDS = 6 - if(server_info != null && server_info.length >= NUM_FIELDS){ - resolve({ - online: true, - version: server_info[2].replace(/\u0000/g, ''), - motd: server_info[3].replace(/\u0000/g, ''), - onlinePlayers: server_info[4].replace(/\u0000/g, ''), - maxPlayers: server_info[5].replace(/\u0000/g,'') - }) - } else { - resolve({ - online: false - }) - } - } - socket.end() - }) - - socket.on('error', (err) => { - socket.destroy() - reject(err) - // ENOTFOUND = Unable to resolve. - // ECONNREFUSED = Unable to connect to port. - }) - }) - +const net = require('net') + +/** + * Retrieves the status of a minecraft server. + * + * @param {string} address The server address. + * @param {number} port Optional. The port of the server. Defaults to 25565. + * @returns {Promise.} A promise which resolves to an object containing + * status information. + */ +exports.getStatus = function(address, port = 25565){ + + if(port == null || port == ''){ + port = 25565 + } + if(typeof port === 'string'){ + port = parseInt(port) + } + + return new Promise((resolve, reject) => { + const socket = net.connect(port, address, () => { + let buff = Buffer.from([0xFE, 0x01]) + socket.write(buff) + }) + + socket.setTimeout(2500, () => { + socket.end() + reject({ + code: 'ETIMEDOUT', + errno: 'ETIMEDOUT', + address, + port + }) + }) + + socket.on('data', (data) => { + if(data != null && data != ''){ + let server_info = data.toString().split('\x00\x00\x00') + const NUM_FIELDS = 6 + if(server_info != null && server_info.length >= NUM_FIELDS){ + resolve({ + online: true, + version: server_info[2].replace(/\u0000/g, ''), + motd: server_info[3].replace(/\u0000/g, ''), + onlinePlayers: server_info[4].replace(/\u0000/g, ''), + maxPlayers: server_info[5].replace(/\u0000/g,'') + }) + } else { + resolve({ + online: false + }) + } + } + socket.end() + }) + + socket.on('error', (err) => { + socket.destroy() + reject(err) + // ENOTFOUND = Unable to resolve. + // ECONNREFUSED = Unable to connect to port. + }) + }) + } \ No newline at end of file