gristlabs_grist-core/app/server/MergedServer.ts

234 lines
7.7 KiB
TypeScript
Raw Normal View History

/**
*
* A version of hosted grist that recombines a home server,
* a doc worker, and a static server on a single port.
*
*/
import {FlexServer, FlexServerOptions} from 'app/server/lib/FlexServer';
import log from 'app/server/lib/log';
import {getGlobalConfig} from "app/server/lib/globalConfig";
// Allowed server types. We'll start one or a combination based on the value of GRIST_SERVERS
// environment variable.
export type ServerType = "home" | "docs" | "static" | "app";
const allServerTypes: ServerType[] = ["home", "docs", "static", "app"];
// Parse a comma-separate list of server types into an array, with validation.
export function parseServerTypes(serverTypes: string|undefined): ServerType[] {
// Split and filter out empty strings (including the one we get when splitting "").
const types = (serverTypes || "").trim().split(',').filter(part => Boolean(part));
// Check that parts is non-empty and only contains valid options.
if (!types.length) {
throw new Error(`No server types; should be a comma-separated list of ${allServerTypes.join(", ")}`);
}
for (const t of types) {
if (!allServerTypes.includes(t as ServerType)) {
throw new Error(`Invalid server type '${t}'; should be in ${allServerTypes.join(", ")}`);
}
}
return types as ServerType[];
}
function checkUserContentPort(): number | null {
// Check whether a port is explicitly set for user content.
if (process.env.GRIST_UNTRUSTED_PORT) {
return parseInt(process.env.GRIST_UNTRUSTED_PORT, 10);
}
// Checks whether to serve user content on same domain but on different port
if (process.env.APP_UNTRUSTED_URL && process.env.APP_HOME_URL) {
const homeUrl = new URL(process.env.APP_HOME_URL);
const pluginUrl = new URL(process.env.APP_UNTRUSTED_URL);
// If the hostname of both home and plugin url are the same,
// but the ports are different
if (homeUrl.hostname === pluginUrl.hostname &&
homeUrl.port !== pluginUrl.port) {
const port = parseInt(pluginUrl.port || '80', 10);
return port;
}
}
return null;
}
interface ServerOptions extends FlexServerOptions {
// If set, messages logged to console (default: false)
// (but if options are not given at all in call to main, logToConsole is set to true)
logToConsole?: boolean;
// If set, documents saved to external storage such as s3 (default is to check environment variables,
// which get set in various ways in dev/test entry points)
externalStorage?: boolean;
}
export class MergedServer {
public static async create(port: number, serverTypes: ServerType[], options: ServerOptions = {}) {
options.settings ??= getGlobalConfig();
const ms = new MergedServer(port, serverTypes, options);
// We need to know early on whether we will be serving plugins or not.
if (ms.hasComponent("home")) {
const userPort = checkUserContentPort();
ms.flexServer.setServesPlugins(userPort !== undefined);
} else {
ms.flexServer.setServesPlugins(false);
}
ms.flexServer.addCleanup();
ms.flexServer.setDirectory();
if (process.env.GRIST_TEST_ROUTER) {
// Add a mock api for adding/removing doc workers from load balancer.
ms.flexServer.testAddRouter();
}
if (ms._options.logToConsole !== false) { ms.flexServer.addLogging(); }
if (ms._options.externalStorage === false) { ms.flexServer.disableExternalStorage(); }
await ms.flexServer.addLoginMiddleware();
if (ms.hasComponent("docs")) {
// It is important that /dw and /v prefixes are accepted (if present) by health check
// in ms case, since they are included in the url registered for the doc worker.
ms.flexServer.stripDocWorkerIdPathPrefixIfPresent();
ms.flexServer.addTagChecker();
}
ms.flexServer.addHealthCheck();
if (ms.hasComponent("home") || ms.hasComponent("app")) {
ms.flexServer.addBootPage();
}
ms.flexServer.denyRequestsIfNotReady();
if (ms.hasComponent("home") || ms.hasComponent("static") || ms.hasComponent("app")) {
ms.flexServer.setDirectory();
}
if (ms.hasComponent("home") || ms.hasComponent("static")) {
ms.flexServer.addStaticAndBowerDirectories();
}
await ms.flexServer.initHomeDBManager();
ms.flexServer.addHosts();
ms.flexServer.addDocWorkerMap();
if (ms.hasComponent("home") || ms.hasComponent("static")) {
await ms.flexServer.addAssetsForPlugins();
}
if (ms.hasComponent("home")) {
ms.flexServer.addEarlyWebhooks();
}
if (ms.hasComponent("home") || ms.hasComponent("docs") || ms.hasComponent("app")) {
ms.flexServer.addSessions();
}
ms.flexServer.addAccessMiddleware();
ms.flexServer.addApiMiddleware();
await ms.flexServer.addBillingMiddleware();
return ms;
}
public readonly flexServer: FlexServer;
private readonly _serverTypes: ServerType[];
private readonly _options: ServerOptions;
private constructor(port: number, serverTypes: ServerType[], options: ServerOptions = {}) {
this._serverTypes = serverTypes;
this._options = options;
this.flexServer = new FlexServer(port, `server(${serverTypes.join(",")})`, options);
}
public hasComponent(serverType: ServerType) {
return this._serverTypes.includes(serverType);
}
public async run() {
try {
await this.flexServer.start();
if (this.hasComponent("home")) {
this.flexServer.addUsage();
if (!this.hasComponent("docs")) {
this.flexServer.addDocApiForwarder();
}
(core) Add installation/site configuration endpoints Summary: A new set of endpoints for managing installation and site configuration have been added: - GET `/api/install/configs/:key` - get the value of the configuration item with the specified key - PUT `/api/install/configs/:key` - set the value of the configuration item with the specified key - body: the JSON value of the configuration item - DELETE `/api/install/configs/:key` - delete the configuration item with the specified key - GET `/api/orgs/:oid/configs/:key` - get the value of the configuration item with the specified key - PUT `/api/orgs/:oid/configs/:key` - set the value of the configuration item with the specified key - body: the JSON value of the configuration item - DELETE `/api/orgs/:oid/configs/:key` - delete the configuration item with the specified key Configuration consists of key/value pairs, where keys are strings (e.g. `"audit_logs_streaming_destinations"`) and values are JSON, including literals like numbers and strings. Only installation admins and site owners are permitted to modify installation and site configuration, respectively. The endpoints are planned to be used in an upcoming feature for enabling audit log streaming for an installation and/or site. Future functionality may use the endpoints as well, which may require extending the current capabilities (e.g. adding support for storing secrets, additional metadata fields, etc.). Test Plan: Server tests Reviewers: paulfitz, jarek Reviewed By: paulfitz, jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D4377
2024-10-16 00:45:10 +00:00
await this.flexServer.addLandingPages();
// Early endpoints use their own json handlers, so they come before
// `addJsonSupport`.
this.flexServer.addEarlyApi();
this.flexServer.addJsonSupport();
this.flexServer.addUpdatesCheck();
// todo: add support for home api to standalone app
this.flexServer.addHomeApi();
this.flexServer.addBillingApi();
this.flexServer.addNotifier();
this.flexServer.addAuditLogger();
await this.flexServer.addTelemetry();
await this.flexServer.addHousekeeper();
await this.flexServer.addLoginRoutes();
this.flexServer.addAccountPage();
this.flexServer.addBillingPages();
this.flexServer.addWelcomePaths();
this.flexServer.addLogEndpoint();
this.flexServer.addGoogleAuthEndpoint();
this.flexServer.addConfigEndpoints();
}
if (this.hasComponent("docs")) {
this.flexServer.addJsonSupport();
this.flexServer.addAuditLogger();
await this.flexServer.addTelemetry();
await this.flexServer.addDoc();
}
if (this.hasComponent("home")) {
this.flexServer.addClientSecrets();
}
this.flexServer.finalizeEndpoints();
await this.flexServer.finalizePlugins(this.hasComponent("home") ? checkUserContentPort() : null);
this.flexServer.checkOptionCombinations();
this.flexServer.summary();
this.flexServer.ready();
} catch(e) {
await this.flexServer.close();
throw e;
}
}
}
export async function startMain() {
try {
const serverTypes = parseServerTypes(process.env.GRIST_SERVERS);
// No defaults for a port, since this server can serve very different purposes.
if (!process.env.GRIST_PORT) {
throw new Error("GRIST_PORT must be specified");
}
const port = parseInt(process.env.GRIST_PORT, 10);
const server = await MergedServer.create(port, serverTypes);
await server.run();
const opt = process.argv[2];
if (opt === '--testingHooks') {
await server.flexServer.addTestingHooks();
}
return server.flexServer;
} catch (e) {
log.error('mergedServer failed to start', e);
process.exit(1);
}
}
if (require.main === module) {
startMain().catch((e) => log.error('mergedServer failed to start', e));
}