From 863fa3b30dd9dae543598e3d87942b5ebc885ee2 Mon Sep 17 00:00:00 2001 From: Jikun <36817715+cjk7989@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:54:34 +0800 Subject: [PATCH] fix: waitOn and fetch IPv4&v6 instead of localhost (#704) * fix: waitOn and fetch IPv4&v6 instead of localhost --- .github/workflows/azuresdkdrop.yml | 2 +- src/core/utils/net.ts | 28 ++++++++++++----- src/msha/handlers/function.handler.ts | 43 ++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/.github/workflows/azuresdkdrop.yml b/.github/workflows/azuresdkdrop.yml index 1deb6a9b..36badb00 100644 --- a/.github/workflows/azuresdkdrop.yml +++ b/.github/workflows/azuresdkdrop.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] - node-version: [14.x, 16.x] + node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v2 diff --git a/src/core/utils/net.ts b/src/core/utils/net.ts index cbe56f39..8dfe694d 100644 --- a/src/core/utils/net.ts +++ b/src/core/utils/net.ts @@ -103,10 +103,12 @@ export async function validateDevServerConfig(url: string | undefined, timeout: const resolvedPortNumber = await isAcceptingTcpConnections({ port, host: hostname }); if (resolvedPortNumber !== 0) { const spinner = ora(); - try { - spinner.start(`Waiting for ${chalk.green(url)} to be ready`); - await waitOn({ - resources: [`tcp:${hostname}:${port}`], + const waitOnOneOfResources = hostname === "localhost" ? [`tcp:127.0.0.1:${port}`, `tcp:[::1]:${port}`] : [`tcp:${hostname}:${port}`]; + spinner.start(`Waiting for ${chalk.green(url)} to be ready`); + + let promises = waitOnOneOfResources.map((resource) => { + return waitOn({ + resources: [resource], delay: 1000, // initial delay in ms, default 0 interval: 100, // poll interval in ms, default 250ms simultaneous: 1, // limit to 1 connection per resource at a time @@ -115,10 +117,22 @@ export async function validateDevServerConfig(url: string | undefined, timeout: window: 1000, // stabilization time in ms, default 750ms strictSSL: false, verbose: false, // force disable verbose logs even if SWA_CLI_DEBUG is enabled - }); - spinner.succeed(`Connected to ${chalk.green(url)} successfully`); + }) + .then(() => { + logger.silly(`Connected to ${resource} successfully`); + return resource; + }) + .catch((err) => { + logger.silly(`Could not connect to ${resource}`); + throw err; + }); + }); + + try { + await Promise.any(promises); + spinner.succeed(`Connected to ${url} successfully`); spinner.clear(); - } catch (err) { + } catch { spinner.fail(); logger.error(`Could not connect to "${url}". Is the server up and running?`, true); } diff --git a/src/msha/handlers/function.handler.ts b/src/msha/handlers/function.handler.ts index 66bc6158..ac1257a3 100644 --- a/src/msha/handlers/function.handler.ts +++ b/src/msha/handlers/function.handler.ts @@ -4,6 +4,7 @@ import httpProxy from "http-proxy"; import fetch from "node-fetch"; import { decodeCookie, logger, logRequest, registerProcessExit, validateCookie } from "../../core"; import { HAS_API, SWA_CLI_API_URI } from "../../core/constants"; +import { parseUrl } from "../../core/utils/net"; import { onConnectionLost } from "../middlewares/request.middleware"; const proxyApi = httpProxy.createProxyServer({ autoRewrite: true }); @@ -53,8 +54,40 @@ function injectClientPrincipalCookies(req: http.ClientRequest) { } } +async function resolveLocalhostByFetching(url: string) { + const { protocol, port } = parseUrl(url); + const fetchOneOfUrl = [`${protocol}//127.0.0.1:${port}`, `${protocol}//[::1]:${port}`]; + + let promises = fetchOneOfUrl.map((Url) => { + return fetch(Url) + .then((response) => { + if (response.ok || response.redirected) { + logger.silly(`Fetch ${Url} successfully`); + return Url; + } else { + logger.silly(`Fetch ${Url} failed with status ${response.status} ${response.statusText}`); + throw new Error(`Fetch ${Url} failed with status ${response.status} ${response.statusText}`); + } + }) + .catch((err) => { + logger.silly(`Could not fetch ${Url}`); + throw err; + }); + }); + + try { + const availableUrl = await Promise.any(promises); + return availableUrl; + } catch { + throw new Error('Error: "localhost" can not be resolved to either IPv4 or IPv6. Please check your network settings.'); + } +} + export function handleFunctionRequest(req: http.IncomingMessage, res: http.ServerResponse) { - const target = SWA_CLI_API_URI(); + let cliApiUri = SWA_CLI_API_URI(); + const { protocol, hostname, port } = parseUrl(cliApiUri); + const target = hostname === "localhost" ? `${protocol}//127.0.0.1:${port}` : cliApiUri; + if (HAS_API) { logger.silly(`function request detected. Proxying to Azure Functions emulator`); logger.silly(` - target: ${chalk.yellow(target)}`); @@ -93,6 +126,14 @@ export function isFunctionRequest(req: http.IncomingMessage, rewritePath?: strin } export async function validateFunctionTriggers(url: string) { + const { hostname } = parseUrl(url); + if (hostname === "localhost") { + try { + url = await resolveLocalhostByFetching(url); + } catch (error: any) { + logger.error(error?.message, true); + } + } try { const functionsResponse = await fetch(`${url}/admin/functions`); const functions = (await functionsResponse.json()) as Array<{ config: { bindings: string[] } }>;