Skip to content

Commit

Permalink
fix: improve glob expressions matching
Browse files Browse the repository at this point in the history
Closes #385
  • Loading branch information
manekinekko committed Mar 14, 2022
1 parent bbecd16 commit 971ef47
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 13 deletions.
90 changes: 83 additions & 7 deletions src/core/utils/glob.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { globToRegExp } from "./glob";
import { globToRegExp, isBalancedCurlyBrackets, isValidGlobExpression } from "./glob";

describe("globToRegExp()", () => {
it("glob = <empty>", () => {
Expand All @@ -24,19 +24,95 @@ describe("globToRegExp()", () => {
expect(globToRegExp("/foo/*")).toBe("\\/foo\\/.*");
});

it("glob = /*.{jpg}", () => {
expect(globToRegExp("/*.{jpg}")).toBe("\\/.*(jpg)");
it("glob = /*.{ext}", () => {
expect(globToRegExp("/*.{ext}")).toBe("\\/.*(ext)");
});

it("glob = /*.{jpg,gif}", () => {
expect(globToRegExp("/*.{jpg,gif}")).toBe("\\/.*(jpg|gif)");
it("glob = /*.{ext,gif}", () => {
expect(globToRegExp("/*.{ext,gif}")).toBe("\\/.*(ext|gif)");
});

it("glob = /foo/*.{jpg,gif}", () => {
expect(globToRegExp("/foo/*.{jpg,gif}")).toBe("\\/foo\\/.*(jpg|gif)");
it("glob = /foo/*.{ext,gif}", () => {
expect(globToRegExp("/foo/*.{ext,gif}")).toBe("\\/foo\\/.*(ext|gif)");
});

it("glob = {foo,bar}.json", () => {
expect(globToRegExp("{foo,bar}.json")).toBe("(foo|bar).json");
});
});

// describe isValidGlobExpression

describe("isValidGlobExpression()", () => {
// valid expressions
["*", "/*", "/foo/*", "/foo/*.ext", "/*.ext", "*.ext", "/foo/*.ext", "/foo/*.{ext}", "/foo/*.{ext,ext}"].forEach((glob) => {
describe("should be TRUE for the following values", () => {
it(`glob = ${glob}`, () => {
expect(isValidGlobExpression(glob)).toBe(true);
});
});
});

// invalid expressions
[
undefined,
"",
"*.*",
"**.*",
"**.**",
"**",
"*/*",
"*/*.ext",
"*.ext/*",
"*/*.ext*",
"*.ext/*.ext",
"/blog/*/management",
"/foo/*.{ext,,,,}",
"/foo/*.{ext,",
"/foo/*.ext,}",
"/foo/*.ext}",
"/foo/*.{}",
"/foo/*.",
"/foo/.",
].forEach((glob) => {
describe("should be FALSE for the following values", () => {
it(`glob = ${glob}`, () => {
expect(isValidGlobExpression(glob)).toBe(false);
});
});
});
});

describe("isBalancedCurlyBrackets()", () => {
it("should be true for {}", () => {
expect(isBalancedCurlyBrackets("{,,,}")).toBe(true);
});

it("should be true for {}{}{}", () => {
expect(isBalancedCurlyBrackets("{,,,}{,,,}{,,,}")).toBe(true);
});

it("should be true for {{}}", () => {
expect(isBalancedCurlyBrackets("{,,,{,,,},,,}")).toBe(true);
});

it("should be false for }{", () => {
expect(isBalancedCurlyBrackets("},,,{")).toBe(false);
});

it("should be false for }{}{", () => {
expect(isBalancedCurlyBrackets("},,,{,,,},,,{")).toBe(false);
});

it("should be false for {", () => {
expect(isBalancedCurlyBrackets("{,,,")).toBe(false);
});

it("should be false for }", () => {
expect(isBalancedCurlyBrackets(",,,}")).toBe(false);
});

it("should be false for {}}{{}", () => {
expect(isBalancedCurlyBrackets("{,,,}},,,{{,,,}")).toBe(false);
});
});
90 changes: 89 additions & 1 deletion src/core/utils/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import chalk from "chalk";
import { logger } from "./logger";

/**
* Turn expression into a valid regex
* Turn expression into a valid regexp
*
* @param glob A string containing a valid wildcard expression
* @returns a string containing a valid RegExp
*/
export function globToRegExp(glob: string | undefined) {
logger.silly(`turning glob expression into valid RegExp`);
Expand All @@ -25,3 +28,88 @@ export function globToRegExp(glob: string | undefined) {

return glob.replace(/\//g, "\\/").replace("*.", ".*").replace("/*", "/.*");
}

/**
* Check if the route rule contains a valid wildcard expression
*
* @param glob A string containing a valid wildcard expression
* @returns true if the glob expression is valid, false otherwise
* @see https://docs.microsoft.com/azure/static-web-apps/configuration#wildcards
*/
export function isValidGlobExpression(glob: string | undefined) {
logger.silly(`checking if glob expression is valid`);
logger.silly(` - glob: ${chalk.yellow(glob)}`);

if (!glob) {
logger.silly(` - glob is empty. Return false`);
return false;
}

if (glob === "*") {
logger.silly(` - glob is *`);
return true;
}

const hasWildcard = glob.includes("*");

if (hasWildcard) {
const paths = glob.split("*");
if (paths.length > 2) {
logger.silly(` - glob has more than one wildcard. Return false`);
return false;
}

const pathBeforeWildcard = paths[0];
if (pathBeforeWildcard && glob.endsWith("*")) {
logger.silly(` - glob ends with *. Return true`);
return true;
}

const pathAfterWildcard = paths[1];
if (pathAfterWildcard) {
logger.silly(` - pathAfterWildcard: ${chalk.yellow(pathAfterWildcard)}`);

if (isBalancedCurlyBrackets(glob) === false) {
logger.silly(` - pathAfterWildcard contains unbalanced { } syntax. Return false`);
return false;
}

// match exactly extensions of type:
// --> /blog/*.html
// --> /blog/*.{html,jpg}
const filesExtensionMatch = pathAfterWildcard.match(/\.(\w+|\{\w+(,\w+)*\})$/);

if (filesExtensionMatch) {
logger.silly(` - pathAfterWildcard match a file extension. Return true`);
return true;
} else {
logger.silly(` - pathAfterWildcard doesn't match a file extension. Return false`);
return false;
}
}
}

return false;
}

/**
* Checks if a string expression has balanced curly brackets
*
* @param str the string expression to be checked
* @returns true if the string expression has balanced curly brackets, false otherwise
*/
export function isBalancedCurlyBrackets(str: string) {
const stack = [];
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === "{") {
stack.push(char);
} else if (char === "}") {
if (stack.length === 0) {
return false;
}
stack.pop();
}
}
return stack.length === 0;
}
12 changes: 9 additions & 3 deletions src/msha/routes-engine/route-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import chalk from "chalk";
import { DEFAULT_CONFIG } from "../../config";
import { logger } from "../../core";
import { AUTH_STATUS, SWA_CLI_APP_PROTOCOL } from "../../core/constants";
import { globToRegExp } from "../../core/utils/glob";
import { globToRegExp, isValidGlobExpression } from "../../core/utils/glob";
import { getIndexHtml } from "./rules/routes";

export function doesRequestPathMatchRoute(
Expand Down Expand Up @@ -46,7 +46,7 @@ export function doesRequestPathMatchRoute(
return true;
}

// Since this is a file request, return now, since tring to get a match by appending /index.html doesn't apply here
// Since this is a file request, return now, since we are trying to get a match by appending /index.html doesn't apply here
if (!route) {
logger.silly(` - route: ${chalk.yellow(route || "<empty>")}`);
logger.silly(` - match: ${chalk.yellow(false)}`);
Expand Down Expand Up @@ -100,11 +100,17 @@ function doesRequestPathMatchWildcardRoute(requestPath: string, requestPathFileW
// before processing regexp which might be expensive
// let's check first if both path and rule start with the same substring
if (pathBeforeWildcard && requestPath.startsWith(pathBeforeWildcard) === false) {
logger.silly(` - substring don't match. Exit`);
logger.silly(` - base path doesn't match. Exit`);

return false;
}

// also, let's check if the route rule doesn't contains a wildcard in the middle of the path
if (isValidGlobExpression(requestPathFileWithWildcard) === false) {
logger.silly(` - route rule contains a wildcard in the middle of the path. Exit`);
return false;
}

// we don't support full globs in the config file.
// add this little utility to convert a wildcard into a valid glob pattern
const regexp = new RegExp(`^${globToRegExp(requestPathFileWithWildcard)}$`);
Expand Down
10 changes: 8 additions & 2 deletions src/msha/routes-engine/rules/navigation-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from "fs";
import type http from "http";
import path from "path";
import { logger } from "../../../core";
import { globToRegExp } from "../../../core/utils/glob";
import { globToRegExp, isValidGlobExpression } from "../../../core/utils/glob";
import { AUTH_STATUS } from "../../../core/constants";
import { doesRequestPathMatchRoute } from "../route-processor";
import { getIndexHtml } from "./routes";
Expand Down Expand Up @@ -50,12 +50,18 @@ export function navigationFallback(req: http.IncomingMessage, res: http.ServerRe

// parse the exclusion rules and match at least one rule
const isMatchedExcludeRule = navigationFallback?.exclude?.some((filter) => {
if (isValidGlobExpression(filter) === false) {
logger.silly(` - invalid rule ${chalk.yellow(filter)}`);
logger.silly(` - mark as no match`);
return false;
}

// we don't support full globs in the config file.
// add this little utility to convert a wildcard into a valid glob pattern
const regexp = new RegExp(`^${globToRegExp(filter)}$`);
const isMatch = regexp.test(originlUrl!);

logger.silly(` - exclude: ${chalk.yellow(filter)}`);
logger.silly(` - rule: ${chalk.yellow(filter)}`);
logger.silly(` - regexp: ${chalk.yellow(regexp)}`);
logger.silly(` - isRegexpMatch: ${chalk.yellow(isMatch)}`);

Expand Down

0 comments on commit 971ef47

Please sign in to comment.