checksum system

This commit is contained in:
Evert Prants 2022-12-01 14:45:34 +02:00
parent 63021515e2
commit 8c2debfed8
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 77 additions and 27 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@squeebot/core",
"version": "3.3.8",
"version": "3.4.0",
"description": "Squeebot v3 core for the execution environment",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@ -25,6 +25,10 @@
"version": {
"type": "string",
"description": "Plugin version"
},
"checksum": {
"type": "string",
"description": "Checksum of the plugin package"
}
},
"required": ["name", "version"]

View File

@ -11,6 +11,7 @@ import { IEnvironment } from '../../types';
import { PluginManager } from '../manager';
import { IPlugin, IPluginManifest } from '../plugin';
import { IRepoPluginDef, IRepository } from './repository';
import { checkChecksum } from '../../util';
/**
* Plugin repository manager
@ -22,25 +23,25 @@ export class RepositoryManager {
/**
* Determine if this repository provides a specific plugin
* @param repo Repository manifest
* @param mf Plugin manifest
* @param repository Repository manifest
* @param pluginManifest Plugin manifest
* @returns Repository plugin definition
*/
public repoProvidesPlugin(
repo: IRepository,
mf: IPluginManifest
repository: IRepository,
pluginManifest: IPluginManifest
): IRepoPluginDef | undefined {
return repo.plugins.find(plugin => plugin.name === mf.name && repo.name === mf.repository);
return repository.plugins.find(plugin => plugin.name === pluginManifest.name && repository.name === pluginManifest.repository);
}
/**
* Find repository providing plugin
* @param pname Plugin name
* @param pluginName Plugin name
* @returns Repository manifest
*/
public findRepoForPlugin(pname: string): IRepository | undefined {
public findRepoForPlugin(pluginName: string): IRepository | undefined {
return Array.from(this.repositories.values()).find(repo =>
repo.plugins.find(plugin => plugin.name === pname) !== undefined
repo.plugins.find(plugin => plugin.name === pluginName) !== undefined
);
}
@ -80,6 +81,11 @@ export class RepositoryManager {
throw new Error('Could not find a repository for a plugin named ' + name);
}
const pluginInRepo = repo.plugins.find((entry) => entry.name === name);
if (!pluginInRepo) {
throw new Error(`Unexpected error: Plugin ${name} not found in repository ${repo.name} manifest, this shouldn't be possible!`);
}
const srcFile = name + '.plugin.tgz';
const pluginPath = repo.url + '/' + srcFile;
const tempdir = await fs.mkdtemp(path.join(this.env.path, '.sbdl'));
@ -87,33 +93,47 @@ export class RepositoryManager {
let manifest: IPluginManifest;
try {
// Download plugin
const save = await httpGET(pluginPath, {}, false, tempfile);
const extract = await tar.x({
// Check for checksum, if included in repository manifest
if (pluginInRepo.checksum) {
if (!await checkChecksum(save, pluginInRepo.checksum)) {
throw new Error(`Plugin ${name} package checksum validation FAILED! Please report this!`);
}
}
// Extract plugin package
await tar.x({
file: tempfile,
C: tempdir,
});
// Extraction failed
const findByName = path.join(tempdir, name);
if (!await fs.pathExists(findByName)) {
throw new Error('Invalid file provided.');
}
// Find manifest
const manifestFile = path.join(findByName, 'plugin.json');
if (!await fs.pathExists(manifestFile)) {
throw new Error('manifest file does not exist.');
}
// Load manifest
const loadManifest = await fs.readJSON(manifestFile);
if (!loadManifest.name || !loadManifest.version) {
throw new Error('Not a valid plugin manifest file.');
}
manifest = loadManifest;
const fp = path.join(this.env.pluginsPath, manifest.name);
manifest.fullPath = fp;
const realPluginPath = path.join(this.env.pluginsPath, manifest.name);
manifest.fullPath = realPluginPath;
manifest.repository = repo.name;
// Write local copy of manifest and copy plugin to the plugins path
await fs.writeJSON(manifestFile, manifest);
await fs.copy(findByName, fp);
await fs.copy(findByName, realPluginPath);
} catch (e) {
await fs.remove(tempdir);
throw e;
@ -205,23 +225,23 @@ export class RepositoryManager {
/**
* Uninstall a repository
* @param repo Repository name or manifest
* @param repository Repository name or manifest
*/
public async uninstallRepository(repo: string | IRepository): Promise<void> {
if (typeof repo === 'string') {
repo = this.getRepoByName(repo) as IRepository;
if (!repo) {
public async uninstallRepository(repository: string | IRepository): Promise<void> {
if (typeof repository === 'string') {
repository = this.getRepoByName(repository) as IRepository;
if (!repository) {
throw new Error('No such repository found!');
}
}
// Remove repository metadata file
const repoFile = path.join(this.env.repositoryPath, repo.name + '.json');
const repoFile = path.join(this.env.repositoryPath, repository.name + '.json');
await fs.remove(repoFile);
this.repositories.delete(repo.name);
this.repositories.delete(repository.name);
// Uninstall all plugins from this repository
for (const plugin of repo.plugins) {
for (const plugin of repository.plugins) {
// Ignore non-installed plugins
try {
await this.uninstallPlugin(plugin.name);

View File

@ -1,6 +1,7 @@
export interface IRepoPluginDef {
name: string;
version: string;
checksum?: string;
}
export interface IRepository {

View File

@ -51,7 +51,7 @@ export class Configuration {
* @param from Internal use only
* @returns Configuration value or default or null
*/
public get(key: string, defval?: any, from?: any): any {
public get<T = string>(key: string, defval?: T, from?: any): T {
if (!from) {
from = this.config;
}
@ -63,24 +63,24 @@ export class Configuration {
if (first != null) {
return this.get(split.slice(1).join('.'), defval, first);
}
return defval;
return defval as T;
}
// Array indexing
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
const match = key.match(/\[(\d+)\]/i);
const realKey = key.substr(0, key.indexOf('['));
const realKey = key.substring(0, key.indexOf('['));
if (match != null) {
const index = parseInt(match[1], 10);
if (from[realKey]) {
return from[realKey][index];
}
}
return defval;
return defval as T;
}
if (from[key] == null) {
return defval;
return defval as T;
}
return from[key];
@ -108,7 +108,7 @@ export class Configuration {
// Array indexing
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
const match = key.match(/\[(\d+)\]/i);
const realKey = key.substr(0, key.indexOf('['));
const realKey = key.substring(0, key.indexOf('['));
if (match != null) {
const index = parseInt(match[1], 10);
if (from[realKey]) {

24
src/util/checksum.ts Normal file
View File

@ -0,0 +1,24 @@
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
export const takeChecksum = (file: string): Promise<string> => {
const hash = crypto.createHash('sha512');
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(path.resolve(file));
readStream.on('data', (chunk) => {
try {
hash.update(chunk);
} catch(e: unknown) {
reject(e);
}
});
readStream.on('close', () => resolve(hash.digest('hex')));
readStream.on('error', (err) => reject(err));
});
};
export const checkChecksum = async (file: string, checksum: string): Promise<boolean> => {
const fileSum = await takeChecksum(file);
return crypto.timingSafeEqual(Buffer.from(fileSum), Buffer.from(checksum));
};

View File

@ -2,6 +2,7 @@ import path from 'path';
export { ScopedEventEmitter } from './events';
export { IProcessData, spawnProcess, execProcess } from './run';
export * from './checksum';
/**
* Load a Node.js module without caching it.