278 lines
No EOL
13 KiB
JavaScript
278 lines
No EOL
13 KiB
JavaScript
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
|