assetguard.js 68 KB

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