Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent redistributing Minecraft official jar #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 160 additions & 8 deletions lib/dl/distribution/DistributionIndexProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { Asset, HashAlgo } from '../Asset'
import { HeliosDistribution, HeliosModule, HeliosServer } from '../../common/distribution/DistributionFactory'
import { Type } from 'helios-distribution-types'
import { mcVersionAtLeast } from '../../common/util/MojangUtils'
import { ensureDir, readJson, writeJson } from 'fs-extra'
import { copyFile, ensureDir, exists, readFile, readJson, writeFile, writeJson } from 'fs-extra'
import StreamZip from 'node-stream-zip'
import { dirname } from 'path'
import { dirname, join } from 'path'
import { spawn } from 'child_process'

export class DistributionIndexProcessor extends IndexProcessor {
const logger = LoggerUtil.getLogger('DistributionIndexProcessor')

private static readonly logger = LoggerUtil.getLogger('DistributionIndexProcessor')
export class DistributionIndexProcessor extends IndexProcessor {

constructor(commonDir: string, protected distribution: HeliosDistribution, protected serverId: string) {
super(commonDir)
Expand Down Expand Up @@ -43,7 +44,7 @@ export class DistributionIndexProcessor extends IndexProcessor {
}

public async postDownload(): Promise<void> {
await this.loadForgeVersionJson()
// no-op
}

private async validateModules(modules: HeliosModule[], accumulator: Asset[]): Promise<void> {
Expand All @@ -67,6 +68,150 @@ export class DistributionIndexProcessor extends IndexProcessor {
}
}

/**
* Install forge with ForgeInstallerCLI.
*
* @param {string} javaExecPath Java path.
* @param {string} wrapperPath ForgeInstallerCLI.jar path.
* @param {(percent: number) => void} onProgress Progress callback.
* @returns {Promise.<void>} An empty promise to indicate the installation has completed.
*/
public async installForge(
javaExecPath: string,
wrapperPath: string,
onProgress: (percent: number) => void
): Promise<void> {
const server: HeliosServer = this.distribution.getServerById(this.serverId)!
if(server == null) {
throw new AssetGuardError(`Invalid server id ${this.serverId}`)
}

const forgeModule = server.modules.find(({ rawModule: { type } }) => type === Type.Forge)

if(forgeModule == null) {
// Before 1.12, Forge was installed already.
return
}

if(!DistributionIndexProcessor.isForgeGradle3(server.rawServer.minecraftVersion, forgeModule.getMavenComponents().version)) {
// Before Forge Gradle 3, Forge was installed already.
return
}

// Forge version is in the format: 1.16.5-36.2.39.json
const forgeVersion = forgeModule.getMavenComponents().version
const forgeManifest = join(this.commonDir, 'versions', forgeVersion, `${forgeVersion}.json`)

// If the forge version already exists, we don't need to install it.
if(await exists(forgeManifest)) {
logger.info('Forge version already exists, skipping installation.')
return
}

// Forge installer is in the format: forge-1.16.5-36.2.39-installer.jar
const installerExecPath = forgeModule.getPath()

// Required for the installer to function.
await writeFile(join(this.commonDir, 'launcher_profiles.json'), JSON.stringify({}))

// Clamped lerp function.
function lerp(a: number, b: number, t: number): number {
if(b == a) return a
if(t < 0) return a
if(t > 1) return b
return a + t * (b - a)
}

// Stages of the forge installer.
const stages = [
{ percent: 0, message: '[Progress.Stage] Extracting json', est: 1 },
{ percent: 2, message: '[Progress.Stage] Considering minecraft client jar', est: 1 },
{ percent: 5, message: '[Progress.Start] Downloading libraries' },
{ percent: 35, message: '[Progress.Start] Created Temporary Directory: ' },
{ percent: 40, message: '[Progress.Start] Building Processors', est: 1 },
{ percent: 45, message: 'Splitting: ', est: 7000, countAll: true },
// for 1.20.1
{ percent: 60, nextPercent: 80, message: ' MainClass: net.minecraftforge.fart.Main', est: 2500, countAll: true },
// for 1.16.5
{ percent: 60, nextPercent: 80, message: ' MainClass: net.md_5.specialsource.SpecialSource', est: 25, countAll: true },
{ percent: 80, message: 'Applying: ', est: 1 },
{ percent: 85, message: ' Checksum: ', est: 120 },
{ percent: 90, message: ' Patching ', est: 1000 },
{ percent: 100, message: '[Progress.Stage] Injecting profile', est: 1 }
]

// Forge installer logs are not very useful, so we need to parse them to get a better progress estimate.
let stage = 0
let startCount = 0
let msgCount = 0
let cliPercent = 0
function onLog(logChunk: string): void {
for (const log of logChunk.split('[Forge Installer] ')) {
if (log.length === 0) continue

logger.debug(`[Forge Installer] ${log}`)
msgCount++

// Progress messages are the most useful, so we can use them to get a better estimate.
const match = log.match(/\[Progress\] (\d+)/)
if (match != null) {
cliPercent = Number(match[1])
}

// Find the matching stage.
const index = stages.findIndex(({ message }) => log.startsWith(message))
if(index !== -1) {
if(index > stage) {
// We've moved to the next stage.
stage = index
startCount = 0
msgCount = 0
cliPercent = 0
} else {
// We're still in the same stage, increment the message count.
startCount++
}
}

// Calculate the progress.
const stageInfo = stages[stage]
const nextPercent = stageInfo.nextPercent
?? (stage+1 < stages.length ? stages[stage+1] : stageInfo).percent
// Count all messages in the current stage if countAll is true.
const estProgress = stageInfo.est
? (stageInfo.countAll ? msgCount : startCount) / stageInfo.est
: cliPercent / 100
const percent = lerp(stageInfo.percent, nextPercent, estProgress)

onProgress(Math.floor(percent))
}
}

logger.info('[Forge Installer] Starting')
await new Promise<void>((resolve, reject) => {
const child = spawn(javaExecPath, ['-jar', wrapperPath, '--installer', installerExecPath, '--target', this.commonDir, '--progress'])
child.stdout.on('data', (data) => onLog(data.toString('utf8') as string))
child.stderr.on('data', (data) => onLog(data.toString('utf8') as string))
child.on('close', (code) => {
logger.info('[Forge Installer]', 'Exited with code', code)
if (code === 0) {
resolve()
} else {
reject(`Forge Installer exited with code ${code}`)
}
})
})

// Forge installer generates a version.json in the format: 1.16.5-forge-36.2.39.json
const [mcVer, forgeVer] = forgeVersion.split('-')
const srcForgeVersion = `${mcVer}-forge-${forgeVer}`
const srcForgeManifest = join(this.commonDir, 'versions', srcForgeVersion, `${srcForgeVersion}.json`)

// Rename json if successful.
await ensureDir(dirname(forgeManifest))
await copyFile(srcForgeManifest, forgeManifest)
}

// TODO Type the return type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async loadForgeVersionJson(): Promise<any> {
Expand All @@ -85,12 +230,19 @@ export class DistributionIndexProcessor extends IndexProcessor {
if(DistributionIndexProcessor.isForgeGradle3(server.rawServer.minecraftVersion, forgeModule.getMavenComponents().version)) {

const versionManifstModule = forgeModule.subModules.find(({ rawModule: { type }}) => type === Type.VersionManifest)
if(versionManifstModule == null) {
throw new AssetGuardError('No Forge version manifest module found!')
if(versionManifstModule != null) {
// For 1.12, the version manifest is in the distribution.json.
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await readJson(versionManifstModule.getPath(), 'utf-8')
}

// Forge version is in the format: 1.16.5-36.2.39.json
const forgeVersion = forgeModule.getMavenComponents().version
const forgeManifest = join(this.commonDir, 'versions', forgeVersion, `${forgeVersion}.json`)

logger.info('Loading forge version json from', forgeManifest)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await readJson(versionManifstModule.getPath(), 'utf-8')
return await readJson(forgeManifest, 'utf-8')

} else {

Expand Down