checksum system
This commit is contained in:
parent
63021515e2
commit
8c2debfed8
@ -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",
|
||||
|
@ -25,6 +25,10 @@
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Plugin version"
|
||||
},
|
||||
"checksum": {
|
||||
"type": "string",
|
||||
"description": "Checksum of the plugin package"
|
||||
}
|
||||
},
|
||||
"required": ["name", "version"]
|
||||
|
@ -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);
|
||||
|
@ -1,6 +1,7 @@
|
||||
export interface IRepoPluginDef {
|
||||
name: string;
|
||||
version: string;
|
||||
checksum?: string;
|
||||
}
|
||||
|
||||
export interface IRepository {
|
||||
|
@ -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
24
src/util/checksum.ts
Normal 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));
|
||||
};
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user