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", "chalk": "^5.0.1",
"commander": "^9.4.0", "commander": "^9.4.0",
"inquirer": "^9.1.0", "inquirer": "^9.1.0",
"node-downloader-helper": "^2.1.2",
"ora": "^6.1.2", "ora": "^6.1.2",
"pino": "^8.3.1", "pino": "^8.3.1",
"string-format": "^2.0.0", "string-format": "^2.0.0",
@ -499,6 +500,17 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" "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": { "node_modules/on-exit-leak-free": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" "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": { "on-exit-leak-free": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", "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", "chalk": "^5.0.1",
"commander": "^9.4.0", "commander": "^9.4.0",
"inquirer": "^9.1.0", "inquirer": "^9.1.0",
"node-downloader-helper": "^2.1.2",
"ora": "^6.1.2", "ora": "^6.1.2",
"pino": "^8.3.1", "pino": "^8.3.1",
"string-format": "^2.0.0", "string-format": "^2.0.0",

View File

@ -1,28 +1,48 @@
import path from "path"; import path from "path";
import * as https from "https"; import {readFileSync, unlinkSync} from "fs";
import {createWriteStream} from "fs";
import DownloadError from "../errors/download_error.js"; import DownloadError from "../errors/download_error.js";
import ModManager from "../mod-manager.js"; import ModManager from "../mod-manager.js";
import {createHash} from "crypto";
import { DownloaderHelper } from "node-downloader-helper";
export default class FileDownloader { export default class FileDownloader {
static downloadMod(version: Version): void { static async downloadMod(version: Version): Promise<void> {
try { try {
// Error out if url is null
if (version.url == null) { if (version.url == null) {
throw new Error("URL was 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 filePath = path.join(ModManager.FilePaths.MODS_FOLDER_PATH, version.fileName);
const writeStream = createWriteStream(filePath); const hash = this.getHashForFile(filePath)
res.pipe(writeStream);
writeStream.on("finish", () => writeStream.close()); if (hash != version.checksum) {
writeStream.on('error', () => { unlinkSync(filePath);
throw new Error("Error while writing file during download") throw new DownloadError("The hash for this file does not match the checksum provided")
}) }
}) }
} catch (e) { } catch (e) {
throw new DownloadError(`Failed to download ${version.fileName} from ${version.url}`) 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, fileName: string,
url: string url: string
versionNumber: 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 SEARCH_URL: string = `${CurseforgeSource.BASE_URL}/mods/search`;
private static readonly GET_MOD_URL: string = `${CurseforgeSource.BASE_URL}/mods/%s` 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 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 MINECRAFT_ID: number = 432;
private static readonly FABRIC_TYPE: number = 4; 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 { return {
modId: id.toString(), modId: id.toString(),
fileName: fileObj.fileName, fileName: fileObj.fileName,
url: downloadUrl, url: downloadUrl,
versionNumber: fileObj.displayName, versionNumber: fileObj.displayName,
dependencies: dependencies dependencies: dependencies,
checksum: checksum
} }
} }
@ -92,7 +102,7 @@ export class CurseforgeSource implements ModSource {
dependencies.push(dependency.modId) dependencies.push(dependency.modId)
} }
} }
FileDownloader.downloadMod(version) await FileDownloader.downloadMod(version)
const mod = { const mod = {
name: await this.getProjectName(version.modId), name: await this.getProjectName(version.modId),

View File

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