addons/isl-server/proxy/serverLifecycle.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ServerChallengeResponse} from './server';
b69ab319
b69ab3110import * as http from 'node:http';
b69ab3111import {type ExistingServerInfo, readExistingServerFile} from './existingServerStateFiles';
b69ab3112import {areTokensEqual} from './proxyUtils';
b69ab3113
b69ab3114/**
b69ab3115 * If it looks like something is serving on `localhost` on the same port,
b69ab3116 * send it a request to verify it's actually ISL.
b69ab3117 * Send it the token we recovered and then validate it responds with
b69ab3118 * the same challenge token we recovered.
b69ab3119 *
b69ab3120 * If the challenge is successful, returns the PID of the server; otherwise,
b69ab3121 * returns null.
b69ab3122 */
b69ab3123export async function checkIfServerIsAliveAndIsISL(
b69ab3124 info: typeof console.info,
b69ab3125 port: number,
b69ab3126 existingServerInfo: ExistingServerInfo,
b69ab3127 silent = false,
b69ab3128): Promise<number | null> {
b69ab3129 let response: unknown;
b69ab3130 try {
b69ab3131 const result = await Promise.race<string>([
b69ab3132 new Promise<string>((res, rej) => {
b69ab3133 const req = http.request(
b69ab3134 {
b69ab3135 hostname: 'localhost',
b69ab3136 port,
b69ab3137 path: `/challenge_authenticity?token=${existingServerInfo.sensitiveToken}`,
b69ab3138 method: 'GET',
b69ab3139 },
b69ab3140 response => {
b69ab3141 response.on('data', d => {
b69ab3142 res(d);
b69ab3143 });
b69ab3144 response.on('error', e => {
b69ab3145 rej(e);
b69ab3146 });
b69ab3147 },
b69ab3148 );
b69ab3149 req.on('error', rej);
b69ab3150 req.end();
b69ab3151 }),
b69ab3152 // Timeout so we don't wait around forever for it.
b69ab3153 // This should always be on localhost and therefore quite fast.
b69ab3154 new Promise<never>((_, rej) => setTimeout(() => rej('timeout'), 500)),
b69ab3155 ]);
b69ab3156
b69ab3157 response = JSON.parse(result);
b69ab3158 } catch (error) {
b69ab3159 if (!silent) {
b69ab3160 info(`error checking if existing Sapling Web server on port ${port} is authentic: `, error);
b69ab3161 }
b69ab3162 // if the request fails for any reason, we don't think it's an ISL server.
b69ab3163 return null;
b69ab3164 }
b69ab3165
b69ab3166 if (!validateServerChallengeResponse(response)) {
b69ab3167 return null;
b69ab3168 }
b69ab3169
b69ab3170 const {challengeToken, pid} = response;
b69ab3171 return areTokensEqual(challengeToken, existingServerInfo.challengeToken) ? pid : null;
b69ab3172}
b69ab3173
b69ab3174/**
b69ab3175 * Try multiple times to read the server data, in case we try to read during the time between the server
b69ab3176 * starting and it writing to the token file.
b69ab3177 */
b69ab3178export async function readExistingServerFileWithRetries(
b69ab3179 port: number,
b69ab3180): Promise<ExistingServerInfo | undefined> {
b69ab3181 let tries = 3;
b69ab3182 while (tries > 0) {
b69ab3183 try {
b69ab3184 // eslint-disable-next-line no-await-in-loop
b69ab3185 return await readExistingServerFile(port);
b69ab3186 } catch (error) {
b69ab3187 sleepMs(500);
b69ab3188 }
b69ab3189 tries--;
b69ab3190 }
b69ab3191 return undefined;
b69ab3192}
b69ab3193
b69ab3194function sleepMs(timeMs: number): Promise<void> {
b69ab3195 return new Promise(res => setTimeout(res, timeMs));
b69ab3196}
b69ab3197
b69ab3198export function validateServerChallengeResponse(v: unknown): v is ServerChallengeResponse {
b69ab3199 return (
b69ab31100 !!v &&
b69ab31101 typeof v === 'object' &&
b69ab31102 typeof (v as ServerChallengeResponse).challengeToken === 'string' &&
b69ab31103 typeof (v as ServerChallengeResponse).pid === 'number'
b69ab31104 );
b69ab31105}