assetguard.js 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638
  1. // Requirements
  2. const AdmZip = require('adm-zip')
  3. const async = require('async')
  4. const child_process = require('child_process')
  5. const crypto = require('crypto')
  6. const EventEmitter = require('events')
  7. const fs = require('fs-extra')
  8. const path = require('path')
  9. const Registry = require('winreg')
  10. const request = require('request')
  11. const tar = require('tar-fs')
  12. const zlib = require('zlib')
  13. const ConfigManager = require('./configmanager')
  14. const DistroManager = require('./distromanager')
  15. const isDev = require('./isdev')
  16. // Constants
  17. const PLATFORM_MAP = {
  18. win32: '-windows-x64.tar.gz',
  19. darwin: '-macosx-x64.tar.gz',
  20. linux: '-linux-x64.tar.gz'
  21. }
  22. // Classes
  23. /** Class representing a base asset. */
  24. class Asset {
  25. /**
  26. * Create an asset.
  27. *
  28. * @param {any} id The id of the asset.
  29. * @param {string} hash The hash value of the asset.
  30. * @param {number} size The size in bytes of the asset.
  31. * @param {string} from The url where the asset can be found.
  32. * @param {string} to The absolute local file path of the asset.
  33. */
  34. constructor(id, hash, size, from, to){
  35. this.id = id
  36. this.hash = hash
  37. this.size = size
  38. this.from = from
  39. this.to = to
  40. }
  41. }
  42. /** Class representing a mojang library. */
  43. class Library extends Asset {
  44. /**
  45. * Converts the process.platform OS names to match mojang's OS names.
  46. */
  47. static mojangFriendlyOS(){
  48. const opSys = process.platform
  49. if (opSys === 'darwin') {
  50. return 'osx'
  51. } else if (opSys === 'win32'){
  52. return 'windows'
  53. } else if (opSys === 'linux'){
  54. return 'linux'
  55. } else {
  56. return 'unknown_os'
  57. }
  58. }
  59. /**
  60. * Checks whether or not a library is valid for download on a particular OS, following
  61. * the rule format specified in the mojang version data index. If the allow property has
  62. * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow
  63. * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING
  64. * the one specified.
  65. *
  66. * If the rules are undefined, the natives property will be checked for a matching entry
  67. * for the current OS.
  68. *
  69. * @param {Array.<Object>} rules The Library's download rules.
  70. * @param {Object} natives The Library's natives object.
  71. * @returns {boolean} True if the Library follows the specified rules, otherwise false.
  72. */
  73. static validateRules(rules, natives){
  74. if(rules == null) {
  75. if(natives == null) {
  76. return true
  77. } else {
  78. return natives[Library.mojangFriendlyOS()] != null
  79. }
  80. }
  81. for(let rule of rules){
  82. const action = rule.action
  83. const osProp = rule.os
  84. if(action != null && osProp != null){
  85. const osName = osProp.name
  86. const osMoj = Library.mojangFriendlyOS()
  87. if(action === 'allow'){
  88. return osName === osMoj
  89. } else if(action === 'disallow'){
  90. return osName !== osMoj
  91. }
  92. }
  93. }
  94. return true
  95. }
  96. }
  97. class DistroModule extends Asset {
  98. /**
  99. * Create a DistroModule. This is for processing,
  100. * not equivalent to the module objects in the
  101. * distro index.
  102. *
  103. * @param {any} id The id of the asset.
  104. * @param {string} hash The hash value of the asset.
  105. * @param {number} size The size in bytes of the asset.
  106. * @param {string} from The url where the asset can be found.
  107. * @param {string} to The absolute local file path of the asset.
  108. * @param {string} type The the module type.
  109. */
  110. constructor(id, hash, size, from, to, type){
  111. super(id, hash, size, from, to)
  112. this.type = type
  113. }
  114. }
  115. /**
  116. * Class representing a download tracker. This is used to store meta data
  117. * about a download queue, including the queue itself.
  118. */
  119. class DLTracker {
  120. /**
  121. * Create a DLTracker
  122. *
  123. * @param {Array.<Asset>} dlqueue An array containing assets queued for download.
  124. * @param {number} dlsize The combined size of each asset in the download queue array.
  125. * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading.
  126. */
  127. constructor(dlqueue, dlsize, callback = null){
  128. this.dlqueue = dlqueue
  129. this.dlsize = dlsize
  130. this.callback = callback
  131. }
  132. }
  133. /**
  134. * Central object class used for control flow. This object stores data about
  135. * categories of downloads. Each category is assigned an identifier with a
  136. * DLTracker object as its value. Combined information is also stored, such as
  137. * the total size of all the queued files in each category. This event is used
  138. * to emit events so that external modules can listen into processing done in
  139. * this module.
  140. */
  141. class AssetGuard extends EventEmitter {
  142. /**
  143. * Create an instance of AssetGuard.
  144. * On creation the object's properties are never-null default
  145. * values. Each identifier is resolved to an empty DLTracker.
  146. *
  147. * @param {string} commonPath The common path for shared game files.
  148. * @param {string} javaexec The path to a java executable which will be used
  149. * to finalize installation.
  150. */
  151. constructor(commonPath, javaexec){
  152. super()
  153. this.totaldlsize = 0
  154. this.progress = 0
  155. this.assets = new DLTracker([], 0)
  156. this.libraries = new DLTracker([], 0)
  157. this.files = new DLTracker([], 0)
  158. this.forge = new DLTracker([], 0)
  159. this.java = new DLTracker([], 0)
  160. this.extractQueue = []
  161. this.commonPath = commonPath
  162. this.javaexec = javaexec
  163. }
  164. // Static Utility Functions
  165. // #region
  166. // Static Hash Validation Functions
  167. // #region
  168. /**
  169. * Calculates the hash for a file using the specified algorithm.
  170. *
  171. * @param {Buffer} buf The buffer containing file data.
  172. * @param {string} algo The hash algorithm.
  173. * @returns {string} The calculated hash in hex.
  174. */
  175. static _calculateHash(buf, algo){
  176. return crypto.createHash(algo).update(buf).digest('hex')
  177. }
  178. /**
  179. * Used to parse a checksums file. This is specifically designed for
  180. * the checksums.sha1 files found inside the forge scala dependencies.
  181. *
  182. * @param {string} content The string content of the checksums file.
  183. * @returns {Object} An object with keys being the file names, and values being the hashes.
  184. */
  185. static _parseChecksumsFile(content){
  186. let finalContent = {}
  187. let lines = content.split('\n')
  188. for(let i=0; i<lines.length; i++){
  189. let bits = lines[i].split(' ')
  190. if(bits[1] == null) {
  191. continue
  192. }
  193. finalContent[bits[1]] = bits[0]
  194. }
  195. return finalContent
  196. }
  197. /**
  198. * Validate that a file exists and matches a given hash value.
  199. *
  200. * @param {string} filePath The path of the file to validate.
  201. * @param {string} algo The hash algorithm to check against.
  202. * @param {string} hash The existing hash to check against.
  203. * @returns {boolean} True if the file exists and calculated hash matches the given hash, otherwise false.
  204. */
  205. static _validateLocal(filePath, algo, hash){
  206. if(fs.existsSync(filePath)){
  207. //No hash provided, have to assume it's good.
  208. if(hash == null){
  209. return true
  210. }
  211. let buf = fs.readFileSync(filePath)
  212. let calcdhash = AssetGuard._calculateHash(buf, algo)
  213. return calcdhash === hash
  214. }
  215. return false
  216. }
  217. /**
  218. * Validates a file in the style used by forge's version index.
  219. *
  220. * @param {string} filePath The path of the file to validate.
  221. * @param {Array.<string>} checksums The checksums listed in the forge version index.
  222. * @returns {boolean} True if the file exists and the hashes match, otherwise false.
  223. */
  224. static _validateForgeChecksum(filePath, checksums){
  225. if(fs.existsSync(filePath)){
  226. if(checksums == null || checksums.length === 0){
  227. return true
  228. }
  229. let buf = fs.readFileSync(filePath)
  230. let calcdhash = AssetGuard._calculateHash(buf, 'sha1')
  231. let valid = checksums.includes(calcdhash)
  232. if(!valid && filePath.endsWith('.jar')){
  233. valid = AssetGuard._validateForgeJar(filePath, checksums)
  234. }
  235. return valid
  236. }
  237. return false
  238. }
  239. /**
  240. * Validates a forge jar file dependency who declares a checksums.sha1 file.
  241. * This can be an expensive task as it usually requires that we calculate thousands
  242. * of hashes.
  243. *
  244. * @param {Buffer} buf The buffer of the jar file.
  245. * @param {Array.<string>} checksums The checksums listed in the forge version index.
  246. * @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes.
  247. */
  248. static _validateForgeJar(buf, checksums){
  249. // Double pass method was the quickest I found. I tried a version where we store data
  250. // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time.
  251. const hashes = {}
  252. let expected = {}
  253. const zip = new AdmZip(buf)
  254. const zipEntries = zip.getEntries()
  255. //First pass
  256. for(let i=0; i<zipEntries.length; i++){
  257. let entry = zipEntries[i]
  258. if(entry.entryName === 'checksums.sha1'){
  259. expected = AssetGuard._parseChecksumsFile(zip.readAsText(entry))
  260. }
  261. hashes[entry.entryName] = AssetGuard._calculateHash(entry.getData(), 'sha1')
  262. }
  263. if(!checksums.includes(hashes['checksums.sha1'])){
  264. return false
  265. }
  266. //Check against expected
  267. const expectedEntries = Object.keys(expected)
  268. for(let i=0; i<expectedEntries.length; i++){
  269. if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
  270. return false
  271. }
  272. }
  273. return true
  274. }
  275. // #endregion
  276. // Miscellaneous Static Functions
  277. // #region
  278. /**
  279. * Extracts and unpacks a file from .pack.xz format.
  280. *
  281. * @param {Array.<string>} filePaths The paths of the files to be extracted and unpacked.
  282. * @returns {Promise.<void>} An empty promise to indicate the extraction has completed.
  283. */
  284. static _extractPackXZ(filePaths, javaExecutable){
  285. console.log('[PackXZExtract] Starting')
  286. return new Promise((resolve, reject) => {
  287. let libPath
  288. if(isDev){
  289. libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
  290. } else {
  291. if(process.platform === 'darwin'){
  292. libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar')
  293. } else {
  294. libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar')
  295. }
  296. }
  297. const filePath = filePaths.join(',')
  298. const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath])
  299. child.stdout.on('data', (data) => {
  300. console.log('[PackXZExtract]', data.toString('utf8'))
  301. })
  302. child.stderr.on('data', (data) => {
  303. console.log('[PackXZExtract]', data.toString('utf8'))
  304. })
  305. child.on('close', (code, signal) => {
  306. console.log('[PackXZExtract]', 'Exited with code', code)
  307. resolve()
  308. })
  309. })
  310. }
  311. /**
  312. * Function which finalizes the forge installation process. This creates a 'version'
  313. * instance for forge and saves its version.json file into that instance. If that
  314. * instance already exists, the contents of the version.json file are read and returned
  315. * in a promise.
  316. *
  317. * @param {Asset} asset The Asset object representing Forge.
  318. * @param {string} commonPath The common path for shared game files.
  319. * @returns {Promise.<Object>} A promise which resolves to the contents of forge's version.json.
  320. */
  321. static _finalizeForgeAsset(asset, commonPath){
  322. return new Promise((resolve, reject) => {
  323. fs.readFile(asset.to, (err, data) => {
  324. const zip = new AdmZip(data)
  325. const zipEntries = zip.getEntries()
  326. for(let i=0; i<zipEntries.length; i++){
  327. if(zipEntries[i].entryName === 'version.json'){
  328. const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
  329. const versionPath = path.join(commonPath, 'versions', forgeVersion.id)
  330. const versionFile = path.join(versionPath, forgeVersion.id + '.json')
  331. if(!fs.existsSync(versionFile)){
  332. fs.ensureDirSync(versionPath)
  333. fs.writeFileSync(path.join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
  334. resolve(forgeVersion)
  335. } else {
  336. //Read the saved file to allow for user modifications.
  337. resolve(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
  338. }
  339. return
  340. }
  341. }
  342. //We didn't find forge's version.json.
  343. reject('Unable to finalize Forge processing, version.json not found! Has forge changed their format?')
  344. })
  345. })
  346. }
  347. // #endregion
  348. // Static Java Utility
  349. // #region
  350. /**
  351. * @typedef OracleJREData
  352. * @property {string} uri The base uri of the JRE.
  353. * @property {{major: string, update: string, build: string}} version Object containing version information.
  354. */
  355. /**
  356. * Resolves the latest version of Oracle's JRE and parses its download link.
  357. *
  358. * @returns {Promise.<OracleJREData>} Promise which resolved to an object containing the JRE download data.
  359. */
  360. static _latestJREOracle(){
  361. const url = 'https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html'
  362. const regex = /https:\/\/.+?(?=\/java)\/java\/jdk\/([0-9]+u[0-9]+)-(b[0-9]+)\/([a-f0-9]{32})?\/jre-\1/
  363. return new Promise((resolve, reject) => {
  364. request(url, (err, resp, body) => {
  365. if(!err){
  366. const arr = body.match(regex)
  367. const verSplit = arr[1].split('u')
  368. resolve({
  369. uri: arr[0],
  370. version: {
  371. major: verSplit[0],
  372. update: verSplit[1],
  373. build: arr[2]
  374. }
  375. })
  376. } else {
  377. resolve(null)
  378. }
  379. })
  380. })
  381. }
  382. /**
  383. * Returns the path of the OS-specific executable for the given Java
  384. * installation. Supported OS's are win32, darwin, linux.
  385. *
  386. * @param {string} rootDir The root directory of the Java installation.
  387. * @returns {string} The path to the Java executable.
  388. */
  389. static javaExecFromRoot(rootDir){
  390. if(process.platform === 'win32'){
  391. return path.join(rootDir, 'bin', 'javaw.exe')
  392. } else if(process.platform === 'darwin'){
  393. return path.join(rootDir, 'Contents', 'Home', 'bin', 'java')
  394. } else if(process.platform === 'linux'){
  395. return path.join(rootDir, 'bin', 'java')
  396. }
  397. return rootDir
  398. }
  399. /**
  400. * Check to see if the given path points to a Java executable.
  401. *
  402. * @param {string} pth The path to check against.
  403. * @returns {boolean} True if the path points to a Java executable, otherwise false.
  404. */
  405. static isJavaExecPath(pth){
  406. if(process.platform === 'win32'){
  407. return pth.endsWith(path.join('bin', 'javaw.exe'))
  408. } else if(process.platform === 'darwin'){
  409. return pth.endsWith(path.join('bin', 'java'))
  410. } else if(process.platform === 'linux'){
  411. return pth.endsWith(path.join('bin', 'java'))
  412. }
  413. return false
  414. }
  415. /**
  416. * Load Mojang's launcher.json file.
  417. *
  418. * @returns {Promise.<Object>} Promise which resolves to Mojang's launcher.json object.
  419. */
  420. static loadMojangLauncherData(){
  421. return new Promise((resolve, reject) => {
  422. request.get('https://launchermeta.mojang.com/mc/launcher.json', (err, resp, body) => {
  423. if(err){
  424. resolve(null)
  425. } else {
  426. resolve(JSON.parse(body))
  427. }
  428. })
  429. })
  430. }
  431. /**
  432. * Parses a **full** Java Runtime version string and resolves
  433. * the version information. Uses Java 8 formatting.
  434. *
  435. * @param {string} verString Full version string to parse.
  436. * @returns Object containing the version information.
  437. */
  438. static parseJavaRuntimeVersion(verString){
  439. // 1.{major}.0_{update}-b{build}
  440. // ex. 1.8.0_152-b16
  441. const ret = {}
  442. let pts = verString.split('-')
  443. ret.build = parseInt(pts[1].substring(1))
  444. pts = pts[0].split('_')
  445. ret.update = parseInt(pts[1])
  446. ret.major = parseInt(pts[0].split('.')[1])
  447. return ret
  448. }
  449. /**
  450. * Validates the output of a JVM's properties. Currently validates that a JRE is x64
  451. * and that the major = 8, update > 52.
  452. *
  453. * @param {string} stderr The output to validate.
  454. *
  455. * @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
  456. * The validity is stored inside the `valid` property.
  457. */
  458. static _validateJVMProperties(stderr){
  459. const res = stderr
  460. const props = res.split('\n')
  461. const goal = 2
  462. let checksum = 0
  463. const meta = {}
  464. for(let i=0; i<props.length; i++){
  465. if(props[i].indexOf('sun.arch.data.model') > -1){
  466. let arch = props[i].split('=')[1].trim()
  467. arch = parseInt(arch)
  468. console.log(props[i].trim())
  469. if(arch === 64){
  470. meta.arch = arch
  471. ++checksum
  472. if(checksum === goal){
  473. break
  474. }
  475. }
  476. } else if(props[i].indexOf('java.runtime.version') > -1){
  477. let verString = props[i].split('=')[1].trim()
  478. console.log(props[i].trim())
  479. const verOb = AssetGuard.parseJavaRuntimeVersion(verString)
  480. if(verOb.major === 8 && verOb.update > 52){
  481. meta.version = verOb
  482. ++checksum
  483. if(checksum === goal){
  484. break
  485. }
  486. }
  487. }
  488. }
  489. meta.valid = checksum === goal
  490. return meta
  491. }
  492. /**
  493. * Validates that a Java binary is at least 64 bit. This makes use of the non-standard
  494. * command line option -XshowSettings:properties. The output of this contains a property,
  495. * sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported
  496. * in Java 8 and 9. Since this is a non-standard option. This will resolve to true if
  497. * the function's code throws errors. That would indicate that the option is changed or
  498. * removed.
  499. *
  500. * @param {string} binaryExecPath Path to the java executable we wish to validate.
  501. *
  502. * @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
  503. * The validity is stored inside the `valid` property.
  504. */
  505. static _validateJavaBinary(binaryExecPath){
  506. return new Promise((resolve, reject) => {
  507. if(!AssetGuard.isJavaExecPath(binaryExecPath)){
  508. resolve({valid: false})
  509. } else if(fs.existsSync(binaryExecPath)){
  510. child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => {
  511. try {
  512. // Output is stored in stderr?
  513. resolve(this._validateJVMProperties(stderr))
  514. } catch (err){
  515. // Output format might have changed, validation cannot be completed.
  516. resolve({valid: false})
  517. }
  518. })
  519. } else {
  520. resolve({valid: false})
  521. }
  522. })
  523. }
  524. /**
  525. * Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check
  526. * to see if the value points to a path which exists. If the path exits, the path is returned.
  527. *
  528. * @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null.
  529. */
  530. static _scanJavaHome(){
  531. const jHome = process.env.JAVA_HOME
  532. try {
  533. let res = fs.existsSync(jHome)
  534. return res ? jHome : null
  535. } catch (err) {
  536. // Malformed JAVA_HOME property.
  537. return null
  538. }
  539. }
  540. /**
  541. * Scans the registry for 64-bit Java entries. The paths of each entry are added to
  542. * a set and returned. Currently, only Java 8 (1.8) is supported.
  543. *
  544. * @returns {Promise.<Set.<string>>} A promise which resolves to a set of 64-bit Java root
  545. * paths found in the registry.
  546. */
  547. static _scanRegistry(){
  548. return new Promise((resolve, reject) => {
  549. // Keys for Java v9.0.0 and later:
  550. // 'SOFTWARE\\JavaSoft\\JRE'
  551. // 'SOFTWARE\\JavaSoft\\JDK'
  552. // Forge does not yet support Java 9, therefore we do not.
  553. // Keys for Java 1.8 and prior:
  554. const regKeys = [
  555. '\\SOFTWARE\\JavaSoft\\Java Runtime Environment',
  556. '\\SOFTWARE\\JavaSoft\\Java Development Kit'
  557. ]
  558. let keysDone = 0
  559. const candidates = new Set()
  560. for(let i=0; i<regKeys.length; i++){
  561. const key = new Registry({
  562. hive: Registry.HKLM,
  563. key: regKeys[i],
  564. arch: 'x64'
  565. })
  566. key.keyExists((err, exists) => {
  567. if(exists) {
  568. key.keys((err, javaVers) => {
  569. if(err){
  570. keysDone++
  571. console.error(err)
  572. // REG KEY DONE
  573. // DUE TO ERROR
  574. if(keysDone === regKeys.length){
  575. resolve(candidates)
  576. }
  577. } else {
  578. if(javaVers.length === 0){
  579. // REG KEY DONE
  580. // NO SUBKEYS
  581. keysDone++
  582. if(keysDone === regKeys.length){
  583. resolve(candidates)
  584. }
  585. } else {
  586. let numDone = 0
  587. for(let j=0; j<javaVers.length; j++){
  588. const javaVer = javaVers[j]
  589. const vKey = javaVer.key.substring(javaVer.key.lastIndexOf('\\')+1)
  590. // Only Java 8 is supported currently.
  591. if(parseFloat(vKey) === 1.8){
  592. javaVer.get('JavaHome', (err, res) => {
  593. const jHome = res.value
  594. if(jHome.indexOf('(x86)') === -1){
  595. candidates.add(jHome)
  596. }
  597. // SUBKEY DONE
  598. numDone++
  599. if(numDone === javaVers.length){
  600. keysDone++
  601. if(keysDone === regKeys.length){
  602. resolve(candidates)
  603. }
  604. }
  605. })
  606. } else {
  607. // SUBKEY DONE
  608. // NOT JAVA 8
  609. numDone++
  610. if(numDone === javaVers.length){
  611. keysDone++
  612. if(keysDone === regKeys.length){
  613. resolve(candidates)
  614. }
  615. }
  616. }
  617. }
  618. }
  619. }
  620. })
  621. } else {
  622. // REG KEY DONE
  623. // DUE TO NON-EXISTANCE
  624. keysDone++
  625. if(keysDone === regKeys.length){
  626. resolve(candidates)
  627. }
  628. }
  629. })
  630. }
  631. })
  632. }
  633. /**
  634. * See if JRE exists in the Internet Plug-Ins folder.
  635. *
  636. * @returns {string} The path of the JRE if found, otherwise null.
  637. */
  638. static _scanInternetPlugins(){
  639. // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java
  640. const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin'
  641. const res = fs.existsSync(AssetGuard.javaExecFromRoot(pth))
  642. return res ? pth : null
  643. }
  644. /**
  645. * Scan a directory for root JVM folders.
  646. *
  647. * @param {string} scanDir The directory to scan.
  648. * @returns {Promise.<Set.<string>>} A promise which resolves to a set of the discovered
  649. * root JVM folders.
  650. */
  651. static _scanFileSystem(scanDir){
  652. return new Promise((resolve, reject) => {
  653. fs.exists(scanDir, (e) => {
  654. let res = new Set()
  655. if(e){
  656. fs.readdir(scanDir, (err, files) => {
  657. if(err){
  658. resolve(res)
  659. console.log(err)
  660. } else {
  661. let pathsDone = 0
  662. for(let i=0; i<files.length; i++){
  663. const combinedPath = path.join(scanDir, files[i])
  664. const execPath = AssetGuard.javaExecFromRoot(combinedPath)
  665. fs.exists(execPath, (v) => {
  666. if(v){
  667. res.add(combinedPath)
  668. }
  669. ++pathsDone
  670. if(pathsDone === files.length){
  671. resolve(res)
  672. }
  673. })
  674. }
  675. if(pathsDone === files.length){
  676. resolve(res)
  677. }
  678. }
  679. })
  680. } else {
  681. resolve(res)
  682. }
  683. })
  684. })
  685. }
  686. /**
  687. *
  688. * @param {Set.<string>} rootSet A set of JVM root strings to validate.
  689. * @returns {Promise.<Object[]>} A promise which resolves to an array of meta objects
  690. * for each valid JVM root directory.
  691. */
  692. static async _validateJavaRootSet(rootSet){
  693. const rootArr = Array.from(rootSet)
  694. const validArr = []
  695. for(let i=0; i<rootArr.length; i++){
  696. const execPath = AssetGuard.javaExecFromRoot(rootArr[i])
  697. const metaOb = await AssetGuard._validateJavaBinary(execPath)
  698. if(metaOb.valid){
  699. metaOb.execPath = execPath
  700. validArr.push(metaOb)
  701. }
  702. }
  703. return validArr
  704. }
  705. /**
  706. * Sort an array of JVM meta objects. Best candidates are placed before all others.
  707. * Sorts based on version and gives priority to JREs over JDKs if versions match.
  708. *
  709. * @param {Object[]} validArr An array of JVM meta objects.
  710. * @returns {Object[]} A sorted array of JVM meta objects.
  711. */
  712. static _sortValidJavaArray(validArr){
  713. const retArr = validArr.sort((a, b) => {
  714. // Note that Java 9+ uses semver and that will need to be accounted for in
  715. // the future.
  716. if(a.version.major === b.version.major){
  717. if(a.version.update === b.version.update){
  718. if(a.version.build === b.version.build){
  719. // Same version, give priority to JRE.
  720. if(a.execPath.toLowerCase().indexOf('jdk') > -1){
  721. return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1
  722. } else {
  723. return -1
  724. }
  725. } else {
  726. return a.version.build > b.version.build ? -1 : 1
  727. }
  728. } else {
  729. return a.version.update > b.version.update ? -1 : 1
  730. }
  731. } else {
  732. return a.version.major > b.version.major ? -1 : 1
  733. }
  734. })
  735. return retArr
  736. }
  737. /**
  738. * Attempts to find a valid x64 installation of Java on Windows machines.
  739. * Possible paths will be pulled from the registry and the JAVA_HOME environment
  740. * variable. The paths will be sorted with higher versions preceeding lower, and
  741. * JREs preceeding JDKs. The binaries at the sorted paths will then be validated.
  742. * The first validated is returned.
  743. *
  744. * Higher versions > Lower versions
  745. * If versions are equal, JRE > JDK.
  746. *
  747. * @param {string} dataDir The base launcher directory.
  748. * @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
  749. * x64 Java installation. If none are found, null is returned.
  750. */
  751. static async _win32JavaValidate(dataDir){
  752. // Get possible paths from the registry.
  753. let pathSet1 = await AssetGuard._scanRegistry()
  754. if(pathSet1.length === 0){
  755. // Do a manual file system scan of program files.
  756. pathSet1 = AssetGuard._scanFileSystem('C:\\Program Files\\Java')
  757. }
  758. // Get possible paths from the data directory.
  759. const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
  760. // Merge the results.
  761. const uberSet = new Set([...pathSet1, ...pathSet2])
  762. // Validate JAVA_HOME.
  763. const jHome = AssetGuard._scanJavaHome()
  764. if(jHome != null && jHome.indexOf('(x86)') === -1){
  765. uberSet.add(jHome)
  766. }
  767. let pathArr = await AssetGuard._validateJavaRootSet(uberSet)
  768. pathArr = AssetGuard._sortValidJavaArray(pathArr)
  769. if(pathArr.length > 0){
  770. return pathArr[0].execPath
  771. } else {
  772. return null
  773. }
  774. }
  775. /**
  776. * Attempts to find a valid x64 installation of Java on MacOS.
  777. * The system JVM directory is scanned for possible installations.
  778. * The JAVA_HOME enviroment variable and internet plugins directory
  779. * are also scanned and validated.
  780. *
  781. * Higher versions > Lower versions
  782. * If versions are equal, JRE > JDK.
  783. *
  784. * @param {string} dataDir The base launcher directory.
  785. * @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
  786. * x64 Java installation. If none are found, null is returned.
  787. */
  788. static async _darwinJavaValidate(dataDir){
  789. const pathSet1 = await AssetGuard._scanFileSystem('/Library/Java/JavaVirtualMachines')
  790. const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
  791. const uberSet = new Set([...pathSet1, ...pathSet2])
  792. // Check Internet Plugins folder.
  793. const iPPath = AssetGuard._scanInternetPlugins()
  794. if(iPPath != null){
  795. uberSet.add(iPPath)
  796. }
  797. // Check the JAVA_HOME environment variable.
  798. let jHome = AssetGuard._scanJavaHome()
  799. if(jHome != null){
  800. // Ensure we are at the absolute root.
  801. if(jHome.contains('/Contents/Home')){
  802. jHome = jHome.substring(0, jHome.indexOf('/Contents/Home'))
  803. }
  804. uberSet.add(jHome)
  805. }
  806. let pathArr = await AssetGuard._validateJavaRootSet(uberSet)
  807. pathArr = AssetGuard._sortValidJavaArray(pathArr)
  808. if(pathArr.length > 0){
  809. return pathArr[0].execPath
  810. } else {
  811. return null
  812. }
  813. }
  814. /**
  815. * Attempts to find a valid x64 installation of Java on Linux.
  816. * The system JVM directory is scanned for possible installations.
  817. * The JAVA_HOME enviroment variable is also scanned and validated.
  818. *
  819. * Higher versions > Lower versions
  820. * If versions are equal, JRE > JDK.
  821. *
  822. * @param {string} dataDir The base launcher directory.
  823. * @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
  824. * x64 Java installation. If none are found, null is returned.
  825. */
  826. static async _linuxJavaValidate(dataDir){
  827. const pathSet1 = await AssetGuard._scanFileSystem('/usr/lib/jvm')
  828. const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
  829. const uberSet = new Set([...pathSet1, ...pathSet2])
  830. // Validate JAVA_HOME
  831. const jHome = AssetGuard._scanJavaHome()
  832. if(jHome != null){
  833. uberSet.add(jHome)
  834. }
  835. let pathArr = await AssetGuard._validateJavaRootSet(uberSet)
  836. pathArr = AssetGuard._sortValidJavaArray(pathArr)
  837. if(pathArr.length > 0){
  838. return pathArr[0].execPath
  839. } else {
  840. return null
  841. }
  842. }
  843. /**
  844. * Retrieve the path of a valid x64 Java installation.
  845. *
  846. * @param {string} dataDir The base launcher directory.
  847. * @returns {string} A path to a valid x64 Java installation, null if none found.
  848. */
  849. static async validateJava(dataDir){
  850. return await AssetGuard['_' + process.platform + 'JavaValidate'](dataDir)
  851. }
  852. // #endregion
  853. // #endregion
  854. // Validation Functions
  855. // #region
  856. /**
  857. * Loads the version data for a given minecraft version.
  858. *
  859. * @param {string} version The game version for which to load the index data.
  860. * @param {boolean} force Optional. If true, the version index will be downloaded even if it exists locally. Defaults to false.
  861. * @returns {Promise.<Object>} Promise which resolves to the version data object.
  862. */
  863. loadVersionData(version, force = false){
  864. const self = this
  865. return new Promise(async (resolve, reject) => {
  866. const versionPath = path.join(self.commonPath, 'versions', version)
  867. const versionFile = path.join(versionPath, version + '.json')
  868. if(!fs.existsSync(versionFile) || force){
  869. const url = await self._getVersionDataUrl(version)
  870. //This download will never be tracked as it's essential and trivial.
  871. console.log('Preparing download of ' + version + ' assets.')
  872. fs.ensureDirSync(versionPath)
  873. const stream = request(url).pipe(fs.createWriteStream(versionFile))
  874. stream.on('finish', () => {
  875. resolve(JSON.parse(fs.readFileSync(versionFile)))
  876. })
  877. } else {
  878. resolve(JSON.parse(fs.readFileSync(versionFile)))
  879. }
  880. })
  881. }
  882. /**
  883. * Parses Mojang's version manifest and retrieves the url of the version
  884. * data index.
  885. *
  886. * @param {string} version The version to lookup.
  887. * @returns {Promise.<string>} Promise which resolves to the url of the version data index.
  888. * If the version could not be found, resolves to null.
  889. */
  890. _getVersionDataUrl(version){
  891. return new Promise((resolve, reject) => {
  892. request('https://launchermeta.mojang.com/mc/game/version_manifest.json', (error, resp, body) => {
  893. if(error){
  894. reject(error)
  895. } else {
  896. const manifest = JSON.parse(body)
  897. for(let v of manifest.versions){
  898. if(v.id === version){
  899. resolve(v.url)
  900. }
  901. }
  902. resolve(null)
  903. }
  904. })
  905. })
  906. }
  907. // Asset (Category=''') Validation Functions
  908. // #region
  909. /**
  910. * Public asset validation function. This function will handle the validation of assets.
  911. * It will parse the asset index specified in the version data, analyzing each
  912. * asset entry. In this analysis it will check to see if the local file exists and is valid.
  913. * If not, it will be added to the download queue for the 'assets' identifier.
  914. *
  915. * @param {Object} versionData The version data for the assets.
  916. * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
  917. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  918. */
  919. validateAssets(versionData, force = false){
  920. const self = this
  921. return new Promise((resolve, reject) => {
  922. self._assetChainIndexData(versionData, force).then(() => {
  923. resolve()
  924. })
  925. })
  926. }
  927. //Chain the asset tasks to provide full async. The below functions are private.
  928. /**
  929. * Private function used to chain the asset validation process. This function retrieves
  930. * the index data.
  931. * @param {Object} versionData
  932. * @param {boolean} force
  933. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  934. */
  935. _assetChainIndexData(versionData, force = false){
  936. const self = this
  937. return new Promise((resolve, reject) => {
  938. //Asset index constants.
  939. const assetIndex = versionData.assetIndex
  940. const name = assetIndex.id + '.json'
  941. const indexPath = path.join(self.commonPath, 'assets', 'indexes')
  942. const assetIndexLoc = path.join(indexPath, name)
  943. let data = null
  944. if(!fs.existsSync(assetIndexLoc) || force){
  945. console.log('Downloading ' + versionData.id + ' asset index.')
  946. fs.ensureDirSync(indexPath)
  947. const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
  948. stream.on('finish', () => {
  949. data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
  950. self._assetChainValidateAssets(versionData, data).then(() => {
  951. resolve()
  952. })
  953. })
  954. } else {
  955. data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
  956. self._assetChainValidateAssets(versionData, data).then(() => {
  957. resolve()
  958. })
  959. }
  960. })
  961. }
  962. /**
  963. * Private function used to chain the asset validation process. This function processes
  964. * the assets and enqueues missing or invalid files.
  965. * @param {Object} versionData
  966. * @param {boolean} force
  967. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  968. */
  969. _assetChainValidateAssets(versionData, indexData){
  970. const self = this
  971. return new Promise((resolve, reject) => {
  972. //Asset constants
  973. const resourceURL = 'http://resources.download.minecraft.net/'
  974. const localPath = path.join(self.commonPath, 'assets')
  975. const objectPath = path.join(localPath, 'objects')
  976. const assetDlQueue = []
  977. let dlSize = 0
  978. let acc = 0
  979. const total = Object.keys(indexData.objects).length
  980. //const objKeys = Object.keys(data.objects)
  981. async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => {
  982. acc++
  983. self.emit('progress', 'assets', acc, total)
  984. const hash = value.hash
  985. const assetName = path.join(hash.substring(0, 2), hash)
  986. const urlName = hash.substring(0, 2) + '/' + hash
  987. const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName))
  988. if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){
  989. dlSize += (ast.size*1)
  990. assetDlQueue.push(ast)
  991. }
  992. cb()
  993. }, (err) => {
  994. self.assets = new DLTracker(assetDlQueue, dlSize)
  995. resolve()
  996. })
  997. })
  998. }
  999. // #endregion
  1000. // Library (Category=''') Validation Functions
  1001. // #region
  1002. /**
  1003. * Public library validation function. This function will handle the validation of libraries.
  1004. * It will parse the version data, analyzing each library entry. In this analysis, it will
  1005. * check to see if the local file exists and is valid. If not, it will be added to the download
  1006. * queue for the 'libraries' identifier.
  1007. *
  1008. * @param {Object} versionData The version data for the assets.
  1009. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  1010. */
  1011. validateLibraries(versionData){
  1012. const self = this
  1013. return new Promise((resolve, reject) => {
  1014. const libArr = versionData.libraries
  1015. const libPath = path.join(self.commonPath, 'libraries')
  1016. const libDlQueue = []
  1017. let dlSize = 0
  1018. //Check validity of each library. If the hashs don't match, download the library.
  1019. async.eachLimit(libArr, 5, (lib, cb) => {
  1020. if(Library.validateRules(lib.rules, lib.natives)){
  1021. let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))]
  1022. const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
  1023. if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){
  1024. dlSize += (libItm.size*1)
  1025. libDlQueue.push(libItm)
  1026. }
  1027. }
  1028. cb()
  1029. }, (err) => {
  1030. self.libraries = new DLTracker(libDlQueue, dlSize)
  1031. resolve()
  1032. })
  1033. })
  1034. }
  1035. // #endregion
  1036. // Miscellaneous (Category=files) Validation Functions
  1037. // #region
  1038. /**
  1039. * Public miscellaneous mojang file validation function. These files will be enqueued under
  1040. * the 'files' identifier.
  1041. *
  1042. * @param {Object} versionData The version data for the assets.
  1043. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  1044. */
  1045. validateMiscellaneous(versionData){
  1046. const self = this
  1047. return new Promise(async (resolve, reject) => {
  1048. await self.validateClient(versionData)
  1049. await self.validateLogConfig(versionData)
  1050. resolve()
  1051. })
  1052. }
  1053. /**
  1054. * Validate client file - artifact renamed from client.jar to '{version}'.jar.
  1055. *
  1056. * @param {Object} versionData The version data for the assets.
  1057. * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
  1058. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  1059. */
  1060. validateClient(versionData, force = false){
  1061. const self = this
  1062. return new Promise((resolve, reject) => {
  1063. const clientData = versionData.downloads.client
  1064. const version = versionData.id
  1065. const targetPath = path.join(self.commonPath, 'versions', version)
  1066. const targetFile = version + '.jar'
  1067. let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
  1068. if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){
  1069. self.files.dlqueue.push(client)
  1070. self.files.dlsize += client.size*1
  1071. resolve()
  1072. } else {
  1073. resolve()
  1074. }
  1075. })
  1076. }
  1077. /**
  1078. * Validate log config.
  1079. *
  1080. * @param {Object} versionData The version data for the assets.
  1081. * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
  1082. * @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
  1083. */
  1084. validateLogConfig(versionData){
  1085. const self = this
  1086. return new Promise((resolve, reject) => {
  1087. const client = versionData.logging.client
  1088. const file = client.file
  1089. const targetPath = path.join(self.commonPath, 'assets', 'log_configs')
  1090. let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
  1091. if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){
  1092. self.files.dlqueue.push(logConfig)
  1093. self.files.dlsize += logConfig.size*1
  1094. resolve()
  1095. } else {
  1096. resolve()
  1097. }
  1098. })
  1099. }
  1100. // #endregion
  1101. // Distribution (Category=forge) Validation Functions
  1102. // #region
  1103. /**
  1104. * Validate the distribution.
  1105. *
  1106. * @param {Server} server The Server to validate.
  1107. * @returns {Promise.<Object>} A promise which resolves to the server distribution object.
  1108. */
  1109. validateDistribution(server){
  1110. const self = this
  1111. return new Promise((resolve, reject) => {
  1112. self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID())
  1113. resolve(server)
  1114. })
  1115. }
  1116. _parseDistroModules(modules, version, servid){
  1117. let alist = []
  1118. let asize = 0
  1119. for(let ob of modules){
  1120. let obArtifact = ob.getArtifact()
  1121. let obPath = obArtifact.getPath()
  1122. let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, ob.getType())
  1123. const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath
  1124. if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){
  1125. asize += artifact.size*1
  1126. alist.push(artifact)
  1127. if(validationPath !== obPath) this.extractQueue.push(obPath)
  1128. }
  1129. //Recursively process the submodules then combine the results.
  1130. if(ob.getSubModules() != null){
  1131. let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid)
  1132. asize += dltrack.dlsize*1
  1133. alist = alist.concat(dltrack.dlqueue)
  1134. }
  1135. }
  1136. return new DLTracker(alist, asize)
  1137. }
  1138. /**
  1139. * Loads Forge's version.json data into memory for the specified server id.
  1140. *
  1141. * @param {string} server The Server to load Forge data for.
  1142. * @returns {Promise.<Object>} A promise which resolves to Forge's version.json data.
  1143. */
  1144. loadForgeData(server){
  1145. const self = this
  1146. return new Promise(async (resolve, reject) => {
  1147. const modules = server.getModules()
  1148. for(let ob of modules){
  1149. const type = ob.getType()
  1150. if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){
  1151. let obArtifact = ob.getArtifact()
  1152. let obPath = obArtifact.getPath()
  1153. let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type)
  1154. let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath)
  1155. resolve(forgeData)
  1156. return
  1157. }
  1158. }
  1159. reject('No forge module found!')
  1160. })
  1161. }
  1162. _parseForgeLibraries(){
  1163. /* TODO
  1164. * Forge asset validations are already implemented. When there's nothing much
  1165. * to work on, implement forge downloads using forge's version.json. This is to
  1166. * have the code on standby if we ever need it (since it's half implemented already).
  1167. */
  1168. }
  1169. // #endregion
  1170. // Java (Category=''') Validation (download) Functions
  1171. // #region
  1172. _enqueueOracleJRE(dataDir){
  1173. return new Promise((resolve, reject) => {
  1174. AssetGuard._latestJREOracle().then(verData => {
  1175. if(verData != null){
  1176. const combined = verData.uri + PLATFORM_MAP[process.platform]
  1177. const opts = {
  1178. url: combined,
  1179. headers: {
  1180. 'Cookie': 'oraclelicense=accept-securebackup-cookie'
  1181. }
  1182. }
  1183. request.head(opts, (err, resp, body) => {
  1184. if(err){
  1185. resolve(false)
  1186. } else {
  1187. dataDir = path.join(dataDir, 'runtime', 'x64')
  1188. const name = combined.substring(combined.lastIndexOf('/')+1)
  1189. const fDir = path.join(dataDir, name)
  1190. const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir)
  1191. this.java = new DLTracker([jre], jre.size, (a, self) => {
  1192. let h = null
  1193. fs.createReadStream(a.to)
  1194. .on('error', err => console.log(err))
  1195. .pipe(zlib.createGunzip())
  1196. .on('error', err => console.log(err))
  1197. .pipe(tar.extract(dataDir, {
  1198. map: (header) => {
  1199. if(h == null){
  1200. h = header.name
  1201. }
  1202. }
  1203. }))
  1204. .on('error', err => console.log(err))
  1205. .on('finish', () => {
  1206. fs.unlink(a.to, err => {
  1207. if(err){
  1208. console.log(err)
  1209. }
  1210. if(h.indexOf('/') > -1){
  1211. h = h.substring(0, h.indexOf('/'))
  1212. }
  1213. const pos = path.join(dataDir, h)
  1214. self.emit('complete', 'java', AssetGuard.javaExecFromRoot(pos))
  1215. })
  1216. })
  1217. })
  1218. resolve(true)
  1219. }
  1220. })
  1221. } else {
  1222. resolve(false)
  1223. }
  1224. })
  1225. })
  1226. }
  1227. /*_enqueueMojangJRE(dir){
  1228. return new Promise((resolve, reject) => {
  1229. // Mojang does not host the JRE for linux.
  1230. if(process.platform === 'linux'){
  1231. resolve(false)
  1232. }
  1233. AssetGuard.loadMojangLauncherData().then(data => {
  1234. if(data != null) {
  1235. try {
  1236. const mJRE = data[Library.mojangFriendlyOS()]['64'].jre
  1237. const url = mJRE.url
  1238. request.head(url, (err, resp, body) => {
  1239. if(err){
  1240. resolve(false)
  1241. } else {
  1242. const name = url.substring(url.lastIndexOf('/')+1)
  1243. const fDir = path.join(dir, name)
  1244. const jre = new Asset('jre' + mJRE.version, mJRE.sha1, resp.headers['content-length'], url, fDir)
  1245. this.java = new DLTracker([jre], jre.size, a => {
  1246. fs.readFile(a.to, (err, data) => {
  1247. // Data buffer needs to be decompressed from lzma,
  1248. // not really possible using node.js
  1249. })
  1250. })
  1251. }
  1252. })
  1253. } catch (err){
  1254. resolve(false)
  1255. }
  1256. }
  1257. })
  1258. })
  1259. }*/
  1260. // #endregion
  1261. // #endregion
  1262. // Control Flow Functions
  1263. // #region
  1264. /**
  1265. * Initiate an async download process for an AssetGuard DLTracker.
  1266. *
  1267. * @param {string} identifier The identifier of the AssetGuard DLTracker.
  1268. * @param {number} limit Optional. The number of async processes to run in parallel.
  1269. * @returns {boolean} True if the process began, otherwise false.
  1270. */
  1271. startAsyncProcess(identifier, limit = 5){
  1272. const self = this
  1273. const dlTracker = this[identifier]
  1274. const dlQueue = dlTracker.dlqueue
  1275. if(dlQueue.length > 0){
  1276. console.log('DLQueue', dlQueue)
  1277. async.eachLimit(dlQueue, limit, (asset, cb) => {
  1278. fs.ensureDirSync(path.join(asset.to, '..'))
  1279. let req = request(asset.from)
  1280. req.pause()
  1281. req.on('response', (resp) => {
  1282. if(resp.statusCode === 200){
  1283. let doHashCheck = false
  1284. const contentLength = parseInt(resp.headers['content-length'])
  1285. if(contentLength !== asset.size){
  1286. console.log(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`)
  1287. doHashCheck = true
  1288. // Adjust download
  1289. this.totaldlsize -= asset.size
  1290. this.totaldlsize += contentLength
  1291. }
  1292. let writeStream = fs.createWriteStream(asset.to)
  1293. writeStream.on('close', () => {
  1294. if(dlTracker.callback != null){
  1295. dlTracker.callback.apply(dlTracker, [asset, self])
  1296. }
  1297. if(doHashCheck){
  1298. const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash)
  1299. if(v){
  1300. console.log(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`)
  1301. } else {
  1302. console.error(`Hashes do not match, ${asset.id} may be corrupted.`)
  1303. }
  1304. }
  1305. cb()
  1306. })
  1307. req.pipe(writeStream)
  1308. req.resume()
  1309. } else {
  1310. req.abort()
  1311. console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`)
  1312. self.progress += asset.size*1
  1313. self.emit('progress', 'download', self.progress, self.totaldlsize)
  1314. cb()
  1315. }
  1316. })
  1317. req.on('error', (err) => {
  1318. self.emit('error', 'download', err)
  1319. })
  1320. req.on('data', (chunk) => {
  1321. self.progress += chunk.length
  1322. self.emit('progress', 'download', self.progress, self.totaldlsize)
  1323. })
  1324. }, (err) => {
  1325. if(err){
  1326. console.log('An item in ' + identifier + ' failed to process')
  1327. } else {
  1328. console.log('All ' + identifier + ' have been processed successfully')
  1329. }
  1330. //self.totaldlsize -= dlTracker.dlsize
  1331. //self.progress -= dlTracker.dlsize
  1332. self[identifier] = new DLTracker([], 0)
  1333. if(self.progress >= self.totaldlsize) {
  1334. if(self.extractQueue.length > 0){
  1335. self.emit('progress', 'extract', 1, 1)
  1336. //self.emit('extracting')
  1337. AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => {
  1338. self.extractQueue = []
  1339. self.emit('complete', 'download')
  1340. })
  1341. } else {
  1342. self.emit('complete', 'download')
  1343. }
  1344. }
  1345. })
  1346. return true
  1347. } else {
  1348. return false
  1349. }
  1350. }
  1351. /**
  1352. * This function will initiate the download processed for the specified identifiers. If no argument is
  1353. * given, all identifiers will be initiated. Note that in order for files to be processed you need to run
  1354. * the processing function corresponding to that identifier. If you run this function without processing
  1355. * the files, it is likely nothing will be enqueued in the object and processing will complete
  1356. * immediately. Once all downloads are complete, this function will fire the 'complete' event on the
  1357. * global object instance.
  1358. *
  1359. * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit.
  1360. */
  1361. processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){
  1362. return new Promise((resolve, reject) => {
  1363. let shouldFire = true
  1364. // Assign dltracking variables.
  1365. this.totaldlsize = 0
  1366. this.progress = 0
  1367. for(let iden of identifiers){
  1368. this.totaldlsize += this[iden.id].dlsize
  1369. }
  1370. this.once('complete', (data) => {
  1371. resolve()
  1372. })
  1373. for(let iden of identifiers){
  1374. let r = this.startAsyncProcess(iden.id, iden.limit)
  1375. if(r) shouldFire = false
  1376. }
  1377. if(shouldFire){
  1378. this.emit('complete', 'download')
  1379. }
  1380. })
  1381. }
  1382. async validateEverything(serverid, dev = false){
  1383. if(!ConfigManager.isLoaded()){
  1384. ConfigManager.load()
  1385. }
  1386. DistroManager.setDevMode(dev)
  1387. const dI = await DistroManager.pullLocal()
  1388. const server = dI.getServer(serverid)
  1389. // Validate Everything
  1390. await this.validateDistribution(server)
  1391. this.emit('validate', 'distribution')
  1392. const versionData = await this.loadVersionData(server.getMinecraftVersion())
  1393. this.emit('validate', 'version')
  1394. await this.validateAssets(versionData)
  1395. this.emit('validate', 'assets')
  1396. await this.validateLibraries(versionData)
  1397. this.emit('validate', 'libraries')
  1398. await this.validateMiscellaneous(versionData)
  1399. this.emit('validate', 'files')
  1400. await this.processDlQueues()
  1401. //this.emit('complete', 'download')
  1402. const forgeData = await this.loadForgeData(server)
  1403. return {
  1404. versionData,
  1405. forgeData
  1406. }
  1407. }
  1408. // #endregion
  1409. }
  1410. module.exports = {
  1411. AssetGuard,
  1412. Asset,
  1413. Library
  1414. }