mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 05:22:31 +08:00
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data';
|
|
import { config } from '@grafana/runtime';
|
|
|
|
import { transformPluginSourceForCDN } from '../cdn/utils';
|
|
import { resolveWithCache } from '../loader/cache';
|
|
import { isHostedOnCDN, resolveModulePath } from '../loader/utils';
|
|
|
|
import { SandboxEnvironment, SandboxPluginMeta } from './types';
|
|
|
|
function isSameDomainAsHost(url: string): boolean {
|
|
const locationUrl = new URL(window.location.href);
|
|
const paramUrl = new URL(url);
|
|
return locationUrl.host === paramUrl.host;
|
|
}
|
|
|
|
export async function loadScriptIntoSandbox(url: string, sandboxEnv: SandboxEnvironment) {
|
|
let scriptCode = '';
|
|
|
|
// same-domain
|
|
if (isSameDomainAsHost(url)) {
|
|
const response = await fetch(url);
|
|
scriptCode = await response.text();
|
|
//even though this is not loaded via a CDN we need to transform the sourceMapUrl
|
|
scriptCode = transformPluginSourceForCDN({
|
|
url,
|
|
source: scriptCode,
|
|
transformSourceMapURL: true,
|
|
transformAssets: false,
|
|
});
|
|
// cdn loaded
|
|
} else if (isHostedOnCDN(url)) {
|
|
const response = await fetch(url);
|
|
scriptCode = await response.text();
|
|
scriptCode = transformPluginSourceForCDN({
|
|
url,
|
|
source: scriptCode,
|
|
transformSourceMapURL: true,
|
|
transformAssets: true,
|
|
});
|
|
}
|
|
|
|
if (scriptCode.length === 0) {
|
|
throw new Error('Only same domain scripts are allowed in sandboxed plugins');
|
|
}
|
|
|
|
scriptCode = patchPluginAPIs(scriptCode);
|
|
sandboxEnv.evaluate(scriptCode);
|
|
}
|
|
|
|
export async function getPluginCode(meta: SandboxPluginMeta): Promise<string> {
|
|
if (isHostedOnCDN(meta.module)) {
|
|
// Load plugin from CDN, no need for "resolveWithCache" as CDN URLs already include the version
|
|
const url = meta.module;
|
|
const response = await fetch(url);
|
|
|
|
let pluginCode = await response.text();
|
|
if (!verifySRI(pluginCode, meta.moduleHash)) {
|
|
throw new Error('Invalid SRI for plugin module file');
|
|
}
|
|
|
|
pluginCode = transformPluginSourceForCDN({
|
|
url,
|
|
source: pluginCode,
|
|
transformSourceMapURL: true,
|
|
transformAssets: true,
|
|
});
|
|
return pluginCode;
|
|
} else {
|
|
let modulePath = resolveModulePath(meta.module);
|
|
// resolveWithCache will append a query parameter with its version
|
|
// to ensure correct cached version is served for local plugins
|
|
const pluginCodeUrl = resolveWithCache(modulePath);
|
|
const response = await fetch(pluginCodeUrl);
|
|
|
|
let pluginCode = await response.text();
|
|
if (!verifySRI(pluginCode, meta.moduleHash)) {
|
|
throw new Error('Invalid SRI for plugin module file');
|
|
}
|
|
|
|
pluginCode = transformPluginSourceForCDN({
|
|
url: pluginCodeUrl,
|
|
source: pluginCode,
|
|
transformSourceMapURL: true,
|
|
transformAssets: false,
|
|
});
|
|
pluginCode = patchPluginAPIs(pluginCode);
|
|
return pluginCode;
|
|
}
|
|
}
|
|
|
|
async function verifySRI(pluginCode: string, moduleHash?: string): Promise<boolean> {
|
|
if (!config.featureToggles.pluginsSriChecks) {
|
|
return true;
|
|
}
|
|
|
|
if (!moduleHash || moduleHash.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const [algorithm, _] = moduleHash.split('-');
|
|
const cleanAlgorithm = algorithm.replace('sha', 'SHA-');
|
|
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(pluginCode);
|
|
|
|
const digest = await crypto.subtle.digest(cleanAlgorithm, data);
|
|
const actualHash = btoa(String.fromCharCode(...new Uint8Array(digest)));
|
|
|
|
return `${algorithm}-${actualHash}` === moduleHash;
|
|
}
|
|
|
|
function patchPluginAPIs(pluginCode: string): string {
|
|
return pluginCode.replace(/window\.location/gi, 'window.locationSandbox');
|
|
}
|
|
|
|
export function patchSandboxEnvironmentPrototype(sandboxEnvironment: SandboxEnvironment) {
|
|
// same as https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/vector.ts#L16
|
|
// Array is a "reflective" type in Near-membrane and doesn't get an identify continuity
|
|
sandboxEnvironment.evaluate(
|
|
`${patchArrayVectorProrotypeMethods.toString()};${patchArrayVectorProrotypeMethods.name}()`
|
|
);
|
|
}
|
|
|
|
export function getPluginLoadData(pluginId: string): SandboxPluginMeta {
|
|
// find it in datasources
|
|
for (const datasource of Object.values(config.datasources)) {
|
|
if (datasource.type === pluginId) {
|
|
return datasource.meta;
|
|
}
|
|
}
|
|
|
|
//find it in panels
|
|
for (const panel of Object.values(config.panels)) {
|
|
if (panel.id === pluginId) {
|
|
return panel;
|
|
}
|
|
}
|
|
|
|
//find it in apps
|
|
//the information inside the apps object is more limited
|
|
for (const app of Object.values(config.apps)) {
|
|
if (app.id === pluginId) {
|
|
return {
|
|
id: pluginId,
|
|
type: PluginType.app,
|
|
module: app.path,
|
|
moduleHash: app.moduleHash,
|
|
};
|
|
}
|
|
}
|
|
|
|
throw new Error(`Could not find plugin ${pluginId}`);
|
|
}
|