mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
205 lines
5.2 KiB
TypeScript
205 lines
5.2 KiB
TypeScript
import { ApiError } from 'app/common/ApiError';
|
|
import { BootProbeIds, BootProbeResult } from 'app/common/BootProbe';
|
|
import { removeTrailingSlash } from 'app/common/gutil';
|
|
import { expressWrap, jsonErrorHandler } from 'app/server/lib/expressWrap';
|
|
import { GristServer } from 'app/server/lib/GristServer';
|
|
import * as express from 'express';
|
|
import fetch from 'node-fetch';
|
|
|
|
/**
|
|
* Self-diagnostics useful when installing Grist.
|
|
*/
|
|
export class BootProbes {
|
|
// List of probes.
|
|
public _probes = new Array<Probe>();
|
|
|
|
// Probes indexed by id.
|
|
public _probeById = new Map<string, Probe>();
|
|
|
|
public constructor(private _app: express.Application,
|
|
private _server: GristServer,
|
|
private _base: string,
|
|
private _middleware: express.Handler[] = []) {
|
|
this._addProbes();
|
|
}
|
|
|
|
public addEndpoints() {
|
|
// Return a list of available probes.
|
|
this._app.use(`${this._base}/probe$`,
|
|
...this._middleware,
|
|
expressWrap(async (_, res) => {
|
|
res.json({
|
|
'probes': this._probes.map(probe => {
|
|
return { id: probe.id, name: probe.name };
|
|
}),
|
|
});
|
|
}));
|
|
|
|
// Return result of running an individual probe.
|
|
this._app.use(`${this._base}/probe/:probeId`,
|
|
...this._middleware,
|
|
expressWrap(async (req, res) => {
|
|
const probe = this._probeById.get(req.params.probeId);
|
|
if (!probe) {
|
|
throw new ApiError('unknown probe', 400);
|
|
}
|
|
const result = await probe.apply(this._server, req);
|
|
res.json(result);
|
|
}));
|
|
|
|
// Fall-back for errors.
|
|
this._app.use(`${this._base}/probe`, jsonErrorHandler);
|
|
}
|
|
|
|
private _addProbes() {
|
|
this._probes.push(_homeUrlReachableProbe);
|
|
this._probes.push(_statusCheckProbe);
|
|
this._probes.push(_userProbe);
|
|
this._probes.push(_bootProbe);
|
|
this._probes.push(_hostHeaderProbe);
|
|
this._probes.push(_sandboxingProbe);
|
|
this._probeById = new Map(this._probes.map(p => [p.id, p]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An individual probe has an id, a name, an optional description,
|
|
* and a method that returns a probe result.
|
|
*/
|
|
export interface Probe {
|
|
id: BootProbeIds;
|
|
name: string;
|
|
description?: string;
|
|
apply: (server: GristServer, req: express.Request) => Promise<BootProbeResult>;
|
|
}
|
|
|
|
const _homeUrlReachableProbe: Probe = {
|
|
id: 'reachable',
|
|
name: 'Grist is reachable',
|
|
apply: async (server, req) => {
|
|
const url = server.getHomeInternalUrl();
|
|
try {
|
|
const resp = await fetch(url);
|
|
if (resp.status !== 200) {
|
|
throw new ApiError(await resp.text(), resp.status);
|
|
}
|
|
return {
|
|
success: true,
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
details: {
|
|
error: String(e),
|
|
},
|
|
severity: 'fault',
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
const _statusCheckProbe: Probe = {
|
|
id: 'health-check',
|
|
name: 'Built-in Health check',
|
|
apply: async (server, req) => {
|
|
const baseUrl = server.getHomeInternalUrl();
|
|
const url = new URL(baseUrl);
|
|
url.pathname = removeTrailingSlash(url.pathname) + '/status';
|
|
try {
|
|
const resp = await fetch(url);
|
|
if (resp.status !== 200) {
|
|
throw new Error(`Failed with status ${resp.status}`);
|
|
}
|
|
const txt = await resp.text();
|
|
if (!txt.includes('is alive')) {
|
|
throw new Error(`Failed, page has unexpected content`);
|
|
}
|
|
return {
|
|
success: true,
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
error: String(e),
|
|
severity: 'fault',
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|
|
const _userProbe: Probe = {
|
|
id: 'system-user',
|
|
name: 'System user is sane',
|
|
apply: async () => {
|
|
if (process.getuid && process.getuid() === 0) {
|
|
return {
|
|
success: false,
|
|
verdict: 'User appears to be root (UID 0)',
|
|
severity: 'warning',
|
|
};
|
|
} else {
|
|
return {
|
|
success: true,
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|
|
const _bootProbe: Probe = {
|
|
id: 'boot-page',
|
|
name: 'Boot page exposure',
|
|
apply: async (server) => {
|
|
if (!server.hasBoot) {
|
|
return { success: true };
|
|
}
|
|
const maybeSecureEnough = String(process.env.GRIST_BOOT_KEY).length > 10;
|
|
return {
|
|
success: maybeSecureEnough,
|
|
severity: 'hmm',
|
|
};
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Based on:
|
|
* https://github.com/gristlabs/grist-core/issues/228#issuecomment-1803304438
|
|
*
|
|
* When GRIST_SERVE_SAME_ORIGIN is set, requests arriving to Grist need
|
|
* to have an accurate Host header.
|
|
*/
|
|
const _hostHeaderProbe: Probe = {
|
|
id: 'host-header',
|
|
name: 'Host header is sane',
|
|
apply: async (server, req) => {
|
|
const host = req.header('host');
|
|
const url = new URL(server.getHomeUrl(req));
|
|
if (url.hostname === 'localhost') {
|
|
return {
|
|
done: true,
|
|
};
|
|
}
|
|
if (String(url.hostname).toLowerCase() !== String(host).toLowerCase()) {
|
|
return {
|
|
success: false,
|
|
severity: 'hmm',
|
|
};
|
|
}
|
|
return {
|
|
done: true,
|
|
};
|
|
},
|
|
};
|
|
|
|
|
|
const _sandboxingProbe: Probe = {
|
|
id: 'sandboxing',
|
|
name: 'Sandboxing is working',
|
|
apply: async (server, req) => {
|
|
const details = server.getSandboxInfo();
|
|
return {
|
|
success: details?.configured && details?.functional,
|
|
details,
|
|
};
|
|
},
|
|
};
|