aboutsummaryrefslogtreecommitdiff
path: root/packages/internal-helpers/src/remote.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/internal-helpers/src/remote.ts')
-rw-r--r--packages/internal-helpers/src/remote.ts96
1 files changed, 96 insertions, 0 deletions
diff --git a/packages/internal-helpers/src/remote.ts b/packages/internal-helpers/src/remote.ts
new file mode 100644
index 000000000..0023deff8
--- /dev/null
+++ b/packages/internal-helpers/src/remote.ts
@@ -0,0 +1,96 @@
+export type RemotePattern = {
+ hostname?: string;
+ pathname?: string;
+ protocol?: string;
+ port?: string;
+};
+
+export function matchPattern(url: URL, remotePattern: RemotePattern) {
+ return (
+ matchProtocol(url, remotePattern.protocol) &&
+ matchHostname(url, remotePattern.hostname, true) &&
+ matchPort(url, remotePattern.port) &&
+ matchPathname(url, remotePattern.pathname, true)
+ );
+}
+
+export function matchPort(url: URL, port?: string) {
+ return !port || port === url.port;
+}
+
+export function matchProtocol(url: URL, protocol?: string) {
+ return !protocol || protocol === url.protocol.slice(0, -1);
+}
+
+export function matchHostname(
+ url: URL,
+ hostname?: string,
+ allowWildcard?: boolean,
+) {
+ if (!hostname) {
+ return true;
+ } else if (!allowWildcard || !hostname.startsWith("*")) {
+ return hostname === url.hostname;
+ } else if (hostname.startsWith("**.")) {
+ const slicedHostname = hostname.slice(2); // ** length
+ return (
+ slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname)
+ );
+ } else if (hostname.startsWith("*.")) {
+ const slicedHostname = hostname.slice(1); // * length
+ const additionalSubdomains = url.hostname
+ .replace(slicedHostname, "")
+ .split(".")
+ .filter(Boolean);
+ return additionalSubdomains.length === 1;
+ }
+
+ return false;
+}
+
+export function matchPathname(
+ url: URL,
+ pathname?: string,
+ allowWildcard?: boolean,
+) {
+ if (!pathname) {
+ return true;
+ } else if (!allowWildcard || !pathname.endsWith("*")) {
+ return pathname === url.pathname;
+ } else if (pathname.endsWith("/**")) {
+ const slicedPathname = pathname.slice(0, -2); // ** length
+ return (
+ slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname)
+ );
+ } else if (pathname.endsWith("/*")) {
+ const slicedPathname = pathname.slice(0, -1); // * length
+ const additionalPathChunks = url.pathname
+ .replace(slicedPathname, "")
+ .split("/")
+ .filter(Boolean);
+ return additionalPathChunks.length === 1;
+ }
+
+ return false;
+}
+
+export function isRemoteAllowed(
+ src: string,
+ {
+ domains,
+ remotePatterns,
+ }: {
+ domains: string[];
+ remotePatterns: RemotePattern[];
+ },
+): boolean {
+ if (!URL.canParse(src)) {
+ return false;
+ }
+
+ const url = new URL(src);
+ return (
+ domains.some((domain) => matchHostname(url, domain)) ||
+ remotePatterns.some((remotePattern) => matchPattern(url, remotePattern))
+ );
+}