UNPKG

@azure/static-web-apps-cli

Version:
278 lines 13.1 kB
import chalk from "chalk"; import finalhandler from "finalhandler"; import fs from "node:fs"; import path from "node:path"; import serveStatic from "serve-static"; import waitOn from "wait-on"; import { DEFAULT_CONFIG } from "../../config.js"; import { logger, logRequest } from "../../core/utils/logger.js"; import { findSWAConfigFile } from "../../core/utils/user-config.js"; import { parseUrl } from "../../core/utils/net.js"; import { AUTH_STATUS, CUSTOM_URL_SCHEME, IS_APP_DEV_SERVER, SWA_PUBLIC_DIR } from "../../core/constants.js"; import { getAuthBlockResponse, handleAuthRequest, isAuthRequest, isLoginRequest, isLogoutRequest } from "../handlers/auth.handler.js"; import { isDataApiRequest } from "../handlers/dab.handler.js"; import { handleErrorPage } from "../handlers/error-page.handler.js"; import { isFunctionRequest } from "../handlers/function.handler.js"; import { isRequestMethodValid, isRouteRequiringUserRolesCheck, tryGetMatchingRoute } from "../routes-engine/rules/routes.js"; import { isCustomUrl, parseQueryParams } from "../routes-engine/route-processor.js"; import { getResponse } from "./response.middleware.js"; /** * On connection lost handler. Called when a connection to a target host cannot be made or if the remote target is down. * @param req Node.js HTTP request object. * @param res Node.js HTTP response object. * @param target The HTTP host target. * @returns A callback function including an Error object. */ export function onConnectionLost(req, res, target, prefix = "") { prefix = prefix === "" ? prefix : ` ${prefix} `; return (error) => { if (error.message.includes("ECONNREFUSED")) { const statusCode = 502; res.statusCode = statusCode; const uri = `${target}${req.url}`; logger.error(`${prefix}${req.method} ${uri} - ${statusCode} (Bad Gateway)`); } else { logger.error(`${error.message}`); } logger.silly({ error }); res.end(); }; } /** * * @param appLocation The location of the application code, where the application configuration file is located. * @returns The JSON content of the application configuration file defined in the `staticwebapp.config.json` file (or legacy file `routes.json`). * If no configuration file is found, returns `undefined`. * @see https://docs.microsoft.com/azure/static-web-apps/configuration */ export async function handleUserConfig(appLocation) { if (!appLocation || !fs.existsSync(appLocation)) { return; } const runtimeConfigContent = await findSWAConfigFile(appLocation); if (!runtimeConfigContent) { return; } return runtimeConfigContent.content; } /** * Serves static content or proxy requests to a static dev server (when used). * @param req Node.js HTTP request object. * @param res Node.js HTTP response object. * @param proxyApp An `http-proxy` instance. * @param target The root folder of the static app (ie. `output_location`). Or, the HTTP host target, if connecting to a dev server, or */ async function serveStaticOrProxyResponse(req, res, proxyApp, target) { if ([301, 302].includes(res.statusCode)) { res.end(); return; } const customUrl = isCustomUrl(req); logger.silly(`customUrl: ${chalk.yellow(customUrl)}`); if (req.url?.includes("index.html") || customUrl) { // serve index.html or custom pages from user's `outputLocation` logger.silly(`custom page or index.html detected`); // extract user custom page filename req.url = req.url?.replace(CUSTOM_URL_SCHEME, ""); target = DEFAULT_CONFIG.outputLocation; logger.silly(` - url: ${chalk.yellow(req.url)}`); logger.silly(` - statusCode: ${chalk.yellow(res.statusCode)}`); logger.silly(` - target: ${chalk.yellow(target)}`); } const is4xx = res.statusCode >= 400 && res.statusCode < 500; logger.silly(`is4xx: ${is4xx}`); // if the static app is served by a dev server, forward all requests to it. if (IS_APP_DEV_SERVER() && (!is4xx || customUrl)) { logger.silly(`remote dev server detected. Proxying request`); logger.silly(` - url: ${chalk.yellow(req.url)}`); logger.silly(` - code: ${chalk.yellow(res.statusCode)}`); target = DEFAULT_CONFIG.outputLocation; logRequest(req, target); let { protocol, hostname, port } = parseUrl(target); if (hostname === "localhost") { let waitOnOneOfResources = [`tcp:127.0.0.1:${port}`, `tcp:localhost:${port}`]; let promises = waitOnOneOfResources.map((resource) => { return waitOn({ resources: [resource], interval: 100, // poll interval in ms, default 250ms simultaneous: 1, // limit to 1 connection per resource at a time timeout: 60000, // timeout in ms, default Infinity strictSSL: false, verbose: false, // force disable verbose logs even if SWA_CLI_DEBUG is enabled }) .then(() => { logger.silly(`Connected to ${resource} successfully`); return resource; }) .catch((err) => { logger.silly(`Could not connect to ${resource}`); throw err; }); }); try { const availableUrl = await Promise.any(promises); logger.silly(`${target} validated successfully`); target = protocol + "//" + availableUrl.slice(4); } catch { logger.error(`Could not connect to "${target}". Is the server up and running?`); } } proxyApp.web(req, res, { target, secure: false, toProxy: true, }, onConnectionLost(req, res, target)); proxyApp.once("proxyRes", (proxyRes) => { logger.silly(`getting response from dev server`); logRequest(req, target, proxyRes.statusCode); }); } else { // not a dev server // run one last check before serving the page: // if the requested file is not found on disk // send our SWA 404 default page instead of serve-static's one. let file = null; let fileInOutputLocation = null; let existsInOutputLocation = false; target = DEFAULT_CONFIG.outputLocation; if (target) { fileInOutputLocation = path.join(target, req.url); existsInOutputLocation = fs.existsSync(fileInOutputLocation); logger.silly(`checking if file exists in user's output location`); logger.silly(` - file: ${chalk.yellow(fileInOutputLocation)}`); logger.silly(` - exists: ${chalk.yellow(existsInOutputLocation)}`); } if (existsInOutputLocation === false) { // file doesn't exist in the user's `outputLocation` // check in the cli public dir target = SWA_PUBLIC_DIR; logger.silly(`checking if file exists in CLI public dir`); const fileInCliPublicDir = path.join(target, req.url); const existsInCliPublicDir = fs.existsSync(fileInCliPublicDir); logger.silly(` - file: ${chalk.yellow(fileInCliPublicDir)}`); logger.silly(` - exists: ${chalk.yellow(existsInCliPublicDir)}`); if (existsInCliPublicDir === false) { req.url = "/404.html"; res.statusCode = 404; target = SWA_PUBLIC_DIR; } else { file = fileInCliPublicDir; } } else { file = fileInOutputLocation; } logger.silly(`serving static content`); logger.silly({ file, url: req.url, code: res.statusCode }); const onerror = (err) => console.error(err); const done = finalhandler(req, res, { onerror }); // serving static content is only possible for GET requests req.method = "GET"; serveStatic(target, { extensions: ["html"] })(req, res, done); } } /** * This functions runs a series of heuristics to determines if a request is a Websocket request. * @param req Node.js HTTP request object. * @returns True if the request is a Websocket request. False otherwise. */ function isWebsocketRequest(req) { // TODO: find a better way of guessing if this is a Websocket request const isSockJs = req.url?.includes("sockjs-node"); const hasWebsocketHeader = req.headers.upgrade?.toLowerCase() === "websocket"; return isSockJs || hasWebsocketHeader; } /** * * @param req Node.js HTTP request object. * @param res Node.js HTTP response object. * @param proxyApp An `http-proxy` instance. * @param userConfig The application configuration file defined in the `staticwebapp.config.json` file (or legacy file `routes.json`). * @returns This middleware mutates the `req` and `res` HTTP objects. */ export async function requestMiddleware(req, res, proxyApp, userConfig) { if (!req.url) { return; } logger.silly(``); logger.silly(`--------------------------------------------------------`); logger.silly(`------------------- processing route -------------------`); logger.silly(`--------------------------------------------------------`); logger.silly(`processing ${chalk.yellow(req.url)}`); if (isWebsocketRequest(req)) { logger.silly(`websocket request detected`); return await serveStaticOrProxyResponse(req, res, proxyApp, DEFAULT_CONFIG.outputLocation); } let target = DEFAULT_CONFIG.outputLocation; logger.silly(`checking for matching route`); const matchingRouteRule = tryGetMatchingRoute(req, userConfig); if (matchingRouteRule) { logger.silly({ matchingRouteRule }); const statusCodeToServe = parseInt(`${matchingRouteRule?.statusCode}`, 10); if ([404, 403, 401].includes(statusCodeToServe)) { logger.silly(` - ${statusCodeToServe} code detected. Exit`); handleErrorPage(req, res, statusCodeToServe, userConfig?.responseOverrides); return await serveStaticOrProxyResponse(req, res, proxyApp, target); } } let authStatus = AUTH_STATUS.NoAuth; const isAuthReq = isAuthRequest(req); logger.silly(`checking auth request`); if (isAuthReq) { logger.silly(` - auth request detected`); return await handleAuthRequest(req, res, matchingRouteRule, userConfig); } else { logger.silly(` - not an auth request`); } logger.silly(`checking function request`); const isFunctionReq = isFunctionRequest(req, matchingRouteRule?.rewrite); if (!isFunctionReq) { logger.silly(` - not a function request`); } logger.silly(`checking data-api request`); const isDataApiReq = isDataApiRequest(req, matchingRouteRule?.rewrite); if (!isDataApiReq) { logger.silly(` - not a data Api request`); } if (!isRequestMethodValid(req, isFunctionReq, isAuthReq, isDataApiReq)) { res.statusCode = 405; return res.end(); } logger.silly(`checking for query params`); const { urlPathnameWithoutQueryParams, url, urlPathnameWithQueryParams } = parseQueryParams(req, matchingRouteRule); logger.silly(`checking rewrite auth login request`); if (urlPathnameWithQueryParams && isLoginRequest(urlPathnameWithoutQueryParams)) { logger.silly(` - auth login dectected`); authStatus = AUTH_STATUS.HostNameAuthLogin; req.url = url.toString(); return await handleAuthRequest(req, res, matchingRouteRule, userConfig); } logger.silly(`checking rewrite auth logout request`); if (urlPathnameWithQueryParams && isLogoutRequest(urlPathnameWithoutQueryParams)) { logger.silly(` - auth logout dectected`); authStatus = AUTH_STATUS.HostNameAuthLogout; req.url = url.toString(); return await handleAuthRequest(req, res, matchingRouteRule, userConfig); } if (!isRouteRequiringUserRolesCheck(req, matchingRouteRule, isFunctionReq, authStatus)) { handleErrorPage(req, res, 401, userConfig?.responseOverrides); return await serveStaticOrProxyResponse(req, res, proxyApp, target); } if (authStatus != AUTH_STATUS.NoAuth && (authStatus != AUTH_STATUS.HostNameAuthLogin || !urlPathnameWithQueryParams)) { if (authStatus == AUTH_STATUS.HostNameAuthLogin && matchingRouteRule) { return getAuthBlockResponse(req, res, userConfig, matchingRouteRule); } return await handleAuthRequest(req, res, matchingRouteRule, userConfig); } if (!getResponse(req, res, matchingRouteRule, userConfig, isFunctionReq, isDataApiReq)) { logger.silly(` - url: ${chalk.yellow(req.url)}`); logger.silly(` - target: ${chalk.yellow(target)}`); await serveStaticOrProxyResponse(req, res, proxyApp, target); } } //# sourceMappingURL=request.middleware.js.map