Check checksums for mod downloads to ensure file integrity

This commit is contained in:
Kallum Jones 2022-08-09 22:21:21 +01:00
parent 40cdbf0aca
commit 73e6ed2edd
No known key found for this signature in database
GPG Key ID: D7F4589C4D7F81A9
6 changed files with 73 additions and 19 deletions

17
package-lock.json generated
View File

@ -15,6 +15,7 @@
"chalk": "^5.0.1",
"commander": "^9.4.0",
"inquirer": "^9.1.0",
"node-downloader-helper": "^2.1.2",
"ora": "^6.1.2",
"pino": "^8.3.1",
"string-format": "^2.0.0",
@ -499,6 +500,17 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"node_modules/node-downloader-helper": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.2.tgz",
"integrity": "sha512-h2QdFMIfy2Arl5R4el6CMQr3NzVMm4uHqeZp0kN8XAK7E8MDXJR74DpFJIuWSvXk4q5LzL/9Z3zsFA3rLgax2Q==",
"bin": {
"ndh": "bin/ndh"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/on-exit-leak-free": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz",
@ -1158,6 +1170,11 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"node-downloader-helper": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.2.tgz",
"integrity": "sha512-h2QdFMIfy2Arl5R4el6CMQr3NzVMm4uHqeZp0kN8XAK7E8MDXJR74DpFJIuWSvXk4q5LzL/9Z3zsFA3rLgax2Q=="
},
"on-exit-leak-free": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz",

View File

@ -17,6 +17,7 @@
"chalk": "^5.0.1",
"commander": "^9.4.0",
"inquirer": "^9.1.0",
"node-downloader-helper": "^2.1.2",
"ora": "^6.1.2",
"pino": "^8.3.1",
"string-format": "^2.0.0",

View File

@ -1,28 +1,48 @@
import path from "path";
import * as https from "https";
import {createWriteStream} from "fs";
import {readFileSync, unlinkSync} from "fs";
import DownloadError from "../errors/download_error.js";
import ModManager from "../mod-manager.js";
import {createHash} from "crypto";
import { DownloaderHelper } from "node-downloader-helper";
export default class FileDownloader {
static downloadMod(version: Version): void {
static async downloadMod(version: Version): Promise<void> {
try {
// Error out if url is null
if (version.url == null) {
throw new Error("URL was null");
}
https.get(version.url, res => {
// Download the file
const downloader = new DownloaderHelper(version.url, ModManager.FilePaths.MODS_FOLDER_PATH, {
fileName: version.fileName
}).on("error", err => {
throw err;
});
await downloader.start()
// Check the checksum
if (version.checksum != undefined || version.checksum != "") {
const filePath = path.join(ModManager.FilePaths.MODS_FOLDER_PATH, version.fileName);
const writeStream = createWriteStream(filePath);
res.pipe(writeStream);
writeStream.on("finish", () => writeStream.close());
writeStream.on('error', () => {
throw new Error("Error while writing file during download")
})
})
const hash = this.getHashForFile(filePath)
if (hash != version.checksum) {
unlinkSync(filePath);
throw new DownloadError("The hash for this file does not match the checksum provided")
}
}
} catch (e) {
throw new DownloadError(`Failed to download ${version.fileName} from ${version.url}`)
}
}
private static getHashForFile(filePath: string) {
const hash = createHash("sha1")
const file = readFileSync(filePath);
hash.update(file);
return hash.digest("hex").toString();
}
}

3
src/mods/mod.d.ts vendored
View File

@ -15,7 +15,8 @@ declare global {
fileName: string,
url: string
versionNumber: string,
dependencies: Array<Version>
dependencies: Array<Version>,
checksum: string
}
}

View File

@ -13,7 +13,7 @@ export class CurseforgeSource implements ModSource {
private static readonly SEARCH_URL: string = `${CurseforgeSource.BASE_URL}/mods/search`;
private static readonly GET_MOD_URL: string = `${CurseforgeSource.BASE_URL}/mods/%s`
private static readonly GET_FILE_URL: string = `${CurseforgeSource.BASE_URL}/mods/%s/files/%s`
private static readonly DOWNLOAD_CDN_URL: string = "https://edge.forgecdn.net/files/%s/%s/%s";
private static readonly DOWNLOAD_CDN_URL: string = "https://mediafiles.forgecdn.net/files/%s/%s/%s";
private static readonly MINECRAFT_ID: number = 432;
private static readonly FABRIC_TYPE: number = 4;
@ -49,14 +49,24 @@ export class CurseforgeSource implements ModSource {
}
}
const downloadUrl = fileObj.downloadUrl != null ? fileObj.downloadUrl : this.constructDownloadUrl(id, fileObj.fileName);
const downloadUrl = fileObj.downloadUrl != null ? fileObj.downloadUrl : this.constructDownloadUrl(fileObj.id, fileObj.fileName);
const hashes = fileObj.hashes;
let checksum: string = "";
for (let hash of hashes) {
// If the algorithm for this hash was sha1
if (hash.algo == 1) {
checksum = hash.value;
}
}
return {
modId: id.toString(),
fileName: fileObj.fileName,
url: downloadUrl,
versionNumber: fileObj.displayName,
dependencies: dependencies
dependencies: dependencies,
checksum: checksum
}
}
@ -92,7 +102,7 @@ export class CurseforgeSource implements ModSource {
dependencies.push(dependency.modId)
}
}
FileDownloader.downloadMod(version)
await FileDownloader.downloadMod(version)
const mod = {
name: await this.getProjectName(version.modId),

View File

@ -61,7 +61,7 @@ export default class ModrinthSource implements ModSource {
dependencies.push(dependency.modId)
}
}
FileDownloader.downloadMod(version)
await FileDownloader.downloadMod(version)
const mod = {
name: await this.getProjectName(version.modId),
@ -128,13 +128,15 @@ export default class ModrinthSource implements ModSource {
}
const latestFile = latestVersion.files[0];
const checksum = latestFile.hashes.sha1;
return {
modId: latestVersion.project_id,
versionNumber: latestVersion.version_number,
fileName: latestFile.filename,
url: latestFile.url,
dependencies: dependencies
dependencies: dependencies,
checksum: checksum
};
}
@ -201,12 +203,15 @@ export default class ModrinthSource implements ModSource {
}
}
const checksum = latestFile.hashes.sha1;
return {
modId: latestVersion.project_id,
versionNumber: latestVersion.version_number,
fileName: latestFile.filename,
url: latestFile.url,
dependencies: dependencies
dependencies: dependencies,
checksum: checksum
};
}
}