checksum system
This commit is contained in:
parent
63021515e2
commit
8c2debfed8
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@squeebot/core",
|
"name": "@squeebot/core",
|
||||||
"version": "3.3.8",
|
"version": "3.4.0",
|
||||||
"description": "Squeebot v3 core for the execution environment",
|
"description": "Squeebot v3 core for the execution environment",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
|
@ -25,6 +25,10 @@
|
|||||||
"version": {
|
"version": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plugin version"
|
"description": "Plugin version"
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Checksum of the plugin package"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "version"]
|
"required": ["name", "version"]
|
||||||
|
@ -11,6 +11,7 @@ import { IEnvironment } from '../../types';
|
|||||||
import { PluginManager } from '../manager';
|
import { PluginManager } from '../manager';
|
||||||
import { IPlugin, IPluginManifest } from '../plugin';
|
import { IPlugin, IPluginManifest } from '../plugin';
|
||||||
import { IRepoPluginDef, IRepository } from './repository';
|
import { IRepoPluginDef, IRepository } from './repository';
|
||||||
|
import { checkChecksum } from '../../util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin repository manager
|
* Plugin repository manager
|
||||||
@ -22,25 +23,25 @@ export class RepositoryManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if this repository provides a specific plugin
|
* Determine if this repository provides a specific plugin
|
||||||
* @param repo Repository manifest
|
* @param repository Repository manifest
|
||||||
* @param mf Plugin manifest
|
* @param pluginManifest Plugin manifest
|
||||||
* @returns Repository plugin definition
|
* @returns Repository plugin definition
|
||||||
*/
|
*/
|
||||||
public repoProvidesPlugin(
|
public repoProvidesPlugin(
|
||||||
repo: IRepository,
|
repository: IRepository,
|
||||||
mf: IPluginManifest
|
pluginManifest: IPluginManifest
|
||||||
): IRepoPluginDef | undefined {
|
): 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
|
* Find repository providing plugin
|
||||||
* @param pname Plugin name
|
* @param pluginName Plugin name
|
||||||
* @returns Repository manifest
|
* @returns Repository manifest
|
||||||
*/
|
*/
|
||||||
public findRepoForPlugin(pname: string): IRepository | undefined {
|
public findRepoForPlugin(pluginName: string): IRepository | undefined {
|
||||||
return Array.from(this.repositories.values()).find(repo =>
|
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);
|
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 srcFile = name + '.plugin.tgz';
|
||||||
const pluginPath = repo.url + '/' + srcFile;
|
const pluginPath = repo.url + '/' + srcFile;
|
||||||
const tempdir = await fs.mkdtemp(path.join(this.env.path, '.sbdl'));
|
const tempdir = await fs.mkdtemp(path.join(this.env.path, '.sbdl'));
|
||||||
@ -87,33 +93,47 @@ export class RepositoryManager {
|
|||||||
|
|
||||||
let manifest: IPluginManifest;
|
let manifest: IPluginManifest;
|
||||||
try {
|
try {
|
||||||
|
// Download plugin
|
||||||
const save = await httpGET(pluginPath, {}, false, tempfile);
|
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,
|
file: tempfile,
|
||||||
C: tempdir,
|
C: tempdir,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extraction failed
|
||||||
const findByName = path.join(tempdir, name);
|
const findByName = path.join(tempdir, name);
|
||||||
if (!await fs.pathExists(findByName)) {
|
if (!await fs.pathExists(findByName)) {
|
||||||
throw new Error('Invalid file provided.');
|
throw new Error('Invalid file provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find manifest
|
||||||
const manifestFile = path.join(findByName, 'plugin.json');
|
const manifestFile = path.join(findByName, 'plugin.json');
|
||||||
if (!await fs.pathExists(manifestFile)) {
|
if (!await fs.pathExists(manifestFile)) {
|
||||||
throw new Error('manifest file does not exist.');
|
throw new Error('manifest file does not exist.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load manifest
|
||||||
const loadManifest = await fs.readJSON(manifestFile);
|
const loadManifest = await fs.readJSON(manifestFile);
|
||||||
if (!loadManifest.name || !loadManifest.version) {
|
if (!loadManifest.name || !loadManifest.version) {
|
||||||
throw new Error('Not a valid plugin manifest file.');
|
throw new Error('Not a valid plugin manifest file.');
|
||||||
}
|
}
|
||||||
manifest = loadManifest;
|
manifest = loadManifest;
|
||||||
const fp = path.join(this.env.pluginsPath, manifest.name);
|
const realPluginPath = path.join(this.env.pluginsPath, manifest.name);
|
||||||
manifest.fullPath = fp;
|
manifest.fullPath = realPluginPath;
|
||||||
manifest.repository = repo.name;
|
manifest.repository = repo.name;
|
||||||
|
|
||||||
|
// Write local copy of manifest and copy plugin to the plugins path
|
||||||
await fs.writeJSON(manifestFile, manifest);
|
await fs.writeJSON(manifestFile, manifest);
|
||||||
await fs.copy(findByName, fp);
|
await fs.copy(findByName, realPluginPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await fs.remove(tempdir);
|
await fs.remove(tempdir);
|
||||||
throw e;
|
throw e;
|
||||||
@ -205,23 +225,23 @@ export class RepositoryManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Uninstall a repository
|
* Uninstall a repository
|
||||||
* @param repo Repository name or manifest
|
* @param repository Repository name or manifest
|
||||||
*/
|
*/
|
||||||
public async uninstallRepository(repo: string | IRepository): Promise<void> {
|
public async uninstallRepository(repository: string | IRepository): Promise<void> {
|
||||||
if (typeof repo === 'string') {
|
if (typeof repository === 'string') {
|
||||||
repo = this.getRepoByName(repo) as IRepository;
|
repository = this.getRepoByName(repository) as IRepository;
|
||||||
if (!repo) {
|
if (!repository) {
|
||||||
throw new Error('No such repository found!');
|
throw new Error('No such repository found!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove repository metadata file
|
// 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);
|
await fs.remove(repoFile);
|
||||||
this.repositories.delete(repo.name);
|
this.repositories.delete(repository.name);
|
||||||
|
|
||||||
// Uninstall all plugins from this repository
|
// Uninstall all plugins from this repository
|
||||||
for (const plugin of repo.plugins) {
|
for (const plugin of repository.plugins) {
|
||||||
// Ignore non-installed plugins
|
// Ignore non-installed plugins
|
||||||
try {
|
try {
|
||||||
await this.uninstallPlugin(plugin.name);
|
await this.uninstallPlugin(plugin.name);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface IRepoPluginDef {
|
export interface IRepoPluginDef {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
checksum?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRepository {
|
export interface IRepository {
|
||||||
|
@ -51,7 +51,7 @@ export class Configuration {
|
|||||||
* @param from Internal use only
|
* @param from Internal use only
|
||||||
* @returns Configuration value or default or null
|
* @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) {
|
if (!from) {
|
||||||
from = this.config;
|
from = this.config;
|
||||||
}
|
}
|
||||||
@ -63,24 +63,24 @@ export class Configuration {
|
|||||||
if (first != null) {
|
if (first != null) {
|
||||||
return this.get(split.slice(1).join('.'), defval, first);
|
return this.get(split.slice(1).join('.'), defval, first);
|
||||||
}
|
}
|
||||||
return defval;
|
return defval as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Array indexing
|
// Array indexing
|
||||||
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
|
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
|
||||||
const match = key.match(/\[(\d+)\]/i);
|
const match = key.match(/\[(\d+)\]/i);
|
||||||
const realKey = key.substr(0, key.indexOf('['));
|
const realKey = key.substring(0, key.indexOf('['));
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
const index = parseInt(match[1], 10);
|
const index = parseInt(match[1], 10);
|
||||||
if (from[realKey]) {
|
if (from[realKey]) {
|
||||||
return from[realKey][index];
|
return from[realKey][index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defval;
|
return defval as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (from[key] == null) {
|
if (from[key] == null) {
|
||||||
return defval;
|
return defval as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
return from[key];
|
return from[key];
|
||||||
@ -108,7 +108,7 @@ export class Configuration {
|
|||||||
// Array indexing
|
// Array indexing
|
||||||
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
|
if (key.indexOf('[') !== -1 && key.indexOf(']') !== -1) {
|
||||||
const match = key.match(/\[(\d+)\]/i);
|
const match = key.match(/\[(\d+)\]/i);
|
||||||
const realKey = key.substr(0, key.indexOf('['));
|
const realKey = key.substring(0, key.indexOf('['));
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
const index = parseInt(match[1], 10);
|
const index = parseInt(match[1], 10);
|
||||||
if (from[realKey]) {
|
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 { ScopedEventEmitter } from './events';
|
||||||
export { IProcessData, spawnProcess, execProcess } from './run';
|
export { IProcessData, spawnProcess, execProcess } from './run';
|
||||||
|
export * from './checksum';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a Node.js module without caching it.
|
* Load a Node.js module without caching it.
|
||||||
|
Loading…
Reference in New Issue
Block a user