mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
d431c1eb63
Summary: This adds a basic sandbox check to the admin panel. It also makes the "probes" used in the boot page available from the admin panel, though they are not yet displayed. The sandbox check is built as a probe. In the interests of time, a lot of steps had to be deferred: * Reconcile fully the admin panel and boot page. Specifically, the admin panel should be equally robust to common configuration problems. * Add tests for the sandbox check. * Generalize to multi-server setups. The read-out will not yet be useful for setups where doc workers and home servers are configured separately. Test Plan: Added new test Reviewers: jarek, georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D4241
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.getHomeUrl(req);
|
|
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.getHomeUrl(req);
|
|
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,
|
|
};
|
|
},
|
|
};
|