mirror of
https://git.bits.team/Bits/mod-manager.git
synced 2024-11-21 21:48:21 -05:00
Check checksums for mod downloads to ensure file integrity
This commit is contained in:
parent
40cdbf0aca
commit
73e6ed2edd
17
package-lock.json
generated
17
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
3
src/mods/mod.d.ts
vendored
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user