From 73e6ed2edd19bcce14ace06f128b1425c38d5621 Mon Sep 17 00:00:00 2001 From: Kallum Jones Date: Tue, 9 Aug 2022 22:21:21 +0100 Subject: [PATCH] Check checksums for mod downloads to ensure file integrity --- package-lock.json | 17 +++++++++++ package.json | 1 + src/io/file_downloder.ts | 42 ++++++++++++++++++++------- src/mods/mod.d.ts | 3 +- src/mods/sources/curseforge_source.ts | 18 +++++++++--- src/mods/sources/modrinth_source.ts | 11 +++++-- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 466cfe3..af36ffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 11f6264..5a0aff1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/io/file_downloder.ts b/src/io/file_downloder.ts index 31e79e7..96d8857 100644 --- a/src/io/file_downloder.ts +++ b/src/io/file_downloder.ts @@ -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 { 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(); + } } \ No newline at end of file diff --git a/src/mods/mod.d.ts b/src/mods/mod.d.ts index 23f66a9..54346be 100644 --- a/src/mods/mod.d.ts +++ b/src/mods/mod.d.ts @@ -15,7 +15,8 @@ declare global { fileName: string, url: string versionNumber: string, - dependencies: Array + dependencies: Array, + checksum: string } } diff --git a/src/mods/sources/curseforge_source.ts b/src/mods/sources/curseforge_source.ts index f582bb4..9fab556 100644 --- a/src/mods/sources/curseforge_source.ts +++ b/src/mods/sources/curseforge_source.ts @@ -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), diff --git a/src/mods/sources/modrinth_source.ts b/src/mods/sources/modrinth_source.ts index 84d9124..1cfb60d 100644 --- a/src/mods/sources/modrinth_source.ts +++ b/src/mods/sources/modrinth_source.ts @@ -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 }; } } \ No newline at end of file