From 0c9bed9de57069572c8dd979d7f36d36c3014075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 9 May 2024 16:46:25 -0400 Subject: [PATCH] BootProbes: add a websocket probe This self-test isn't perfect because we're running it from the backend instead of the frontend. Conceivably the backend might have trouble resolving its own url. Eventually we should move this test or something like it to something that executes in the frontend. --- app/client/models/AdminChecks.ts | 10 ++++++++++ app/common/BootProbe.ts | 3 ++- app/server/lib/BootProbes.ts | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/client/models/AdminChecks.ts b/app/client/models/AdminChecks.ts index da720306..1ad4c87b 100644 --- a/app/client/models/AdminChecks.ts +++ b/app/client/models/AdminChecks.ts @@ -169,6 +169,16 @@ It is good practice not to run Grist as the root user. 'reachable': { info: ` The main page of Grist should be available. +` + }, + + 'websockets': { + info: ` +Websocket connections need HTTP 1.1 and the ability to pass a few +extra headers in order to work. Sometimes a reverse proxy can +interfere with these requirements. See this +documentation for more explanation. ` }, }; diff --git a/app/common/BootProbe.ts b/app/common/BootProbe.ts index 753af1c4..699d3515 100644 --- a/app/common/BootProbe.ts +++ b/app/common/BootProbe.ts @@ -6,7 +6,8 @@ export type BootProbeIds = 'reachable' | 'host-header' | 'sandboxing' | - 'system-user' + 'system-user' | + 'websockets' ; export interface BootProbeResult { diff --git a/app/server/lib/BootProbes.ts b/app/server/lib/BootProbes.ts index 242ab0d8..3d6783f3 100644 --- a/app/server/lib/BootProbes.ts +++ b/app/server/lib/BootProbes.ts @@ -4,6 +4,7 @@ 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 WS from 'ws'; import fetch from 'node-fetch'; /** @@ -58,6 +59,7 @@ export class BootProbes { this._probes.push(_bootProbe); this._probes.push(_hostHeaderProbe); this._probes.push(_sandboxingProbe); + this._probes.push(_webSocketsProbe); this._probeById = new Map(this._probes.map(p => [p.id, p])); } } @@ -104,6 +106,37 @@ const _homeUrlReachableProbe: Probe = { } }; +const _webSocketsProbe: Probe = { + id: 'websockets', + name: 'Can we open a websocket with the server', + apply: async (server, req) => { + return new Promise((resolve) => { + const url = new URL(server.getHomeUrl(req)); + url.protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:'; + const ws = new WS.WebSocket(url.href); + const details: Record = { + url, + }; + ws.on('open', () => { + ws.send('Just nod if you can hear me.'); + resolve({ + success: true, + details, + }); + ws.close(); + }); + ws.on('error', (ev) => { + details.error = ev.message; + resolve({ + success: false, + details, + }); + ws.close(); + }); + }); + } +}; + const _statusCheckProbe: Probe = { id: 'health-check', name: 'Is an internal health check passing',