3.1 KB106 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ServerChallengeResponse} from './server';
9
10import * as http from 'node:http';
11import {type ExistingServerInfo, readExistingServerFile} from './existingServerStateFiles';
12import {areTokensEqual} from './proxyUtils';
13
14/**
15 * If it looks like something is serving on `localhost` on the same port,
16 * send it a request to verify it's actually ISL.
17 * Send it the token we recovered and then validate it responds with
18 * the same challenge token we recovered.
19 *
20 * If the challenge is successful, returns the PID of the server; otherwise,
21 * returns null.
22 */
23export async function checkIfServerIsAliveAndIsISL(
24 info: typeof console.info,
25 port: number,
26 existingServerInfo: ExistingServerInfo,
27 silent = false,
28): Promise<number | null> {
29 let response: unknown;
30 try {
31 const result = await Promise.race<string>([
32 new Promise<string>((res, rej) => {
33 const req = http.request(
34 {
35 hostname: 'localhost',
36 port,
37 path: `/challenge_authenticity?token=${existingServerInfo.sensitiveToken}`,
38 method: 'GET',
39 },
40 response => {
41 response.on('data', d => {
42 res(d);
43 });
44 response.on('error', e => {
45 rej(e);
46 });
47 },
48 );
49 req.on('error', rej);
50 req.end();
51 }),
52 // Timeout so we don't wait around forever for it.
53 // This should always be on localhost and therefore quite fast.
54 new Promise<never>((_, rej) => setTimeout(() => rej('timeout'), 500)),
55 ]);
56
57 response = JSON.parse(result);
58 } catch (error) {
59 if (!silent) {
60 info(`error checking if existing Sapling Web server on port ${port} is authentic: `, error);
61 }
62 // if the request fails for any reason, we don't think it's an ISL server.
63 return null;
64 }
65
66 if (!validateServerChallengeResponse(response)) {
67 return null;
68 }
69
70 const {challengeToken, pid} = response;
71 return areTokensEqual(challengeToken, existingServerInfo.challengeToken) ? pid : null;
72}
73
74/**
75 * Try multiple times to read the server data, in case we try to read during the time between the server
76 * starting and it writing to the token file.
77 */
78export async function readExistingServerFileWithRetries(
79 port: number,
80): Promise<ExistingServerInfo | undefined> {
81 let tries = 3;
82 while (tries > 0) {
83 try {
84 // eslint-disable-next-line no-await-in-loop
85 return await readExistingServerFile(port);
86 } catch (error) {
87 sleepMs(500);
88 }
89 tries--;
90 }
91 return undefined;
92}
93
94function sleepMs(timeMs: number): Promise<void> {
95 return new Promise(res => setTimeout(res, timeMs));
96}
97
98export function validateServerChallengeResponse(v: unknown): v is ServerChallengeResponse {
99 return (
100 !!v &&
101 typeof v === 'object' &&
102 typeof (v as ServerChallengeResponse).challengeToken === 'string' &&
103 typeof (v as ServerChallengeResponse).pid === 'number'
104 );
105}
106