/** * * 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(); } this.flexServer.addJsonSupport(); this.flexServer.addUpdatesCheck(); await this.flexServer.addLandingPages(); // 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.addInstallEndpoints(); 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(); // Some tests have their timing perturbed by having this earlier // TODO: update those tests. if (this.hasComponent("docs")) { await this.flexServer.checkSandbox(); } } 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)); }