@cloudflare/pages-shared 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-server/handler.js +395 -0
- package/dist/asset-server/metadata.js +0 -0
- package/dist/asset-server/patchUrl.js +15 -0
- package/dist/asset-server/responses.js +123 -0
- package/dist/asset-server/rulesEngine.js +44 -0
- package/dist/environment-polyfills/index.js +10 -0
- package/dist/environment-polyfills/miniflare.js +13 -0
- package/dist/environment-polyfills/types.js +1 -0
- package/dist/metadata-generator/createMetadataObject.js +1 -1
- package/package.json +10 -3
- package/src/asset-server/handler.ts +600 -0
- package/src/asset-server/metadata.ts +66 -0
- package/src/asset-server/patchUrl.ts +22 -0
- package/src/asset-server/responses.ts +152 -0
- package/src/asset-server/rulesEngine.ts +82 -0
- package/src/environment-polyfills/index.ts +14 -0
- package/src/environment-polyfills/miniflare.ts +14 -0
- package/src/environment-polyfills/types.ts +44 -0
- package/src/index.ts +1 -0
- package/src/metadata-generator/constants.ts +15 -0
- package/src/metadata-generator/createMetadataObject.ts +156 -0
- package/src/metadata-generator/parseHeaders.ts +168 -0
- package/src/metadata-generator/parseRedirects.ts +129 -0
- package/src/metadata-generator/types.ts +89 -0
- package/src/metadata-generator/validateURL.ts +57 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
type HeadersInit = ConstructorParameters<typeof Headers>[0];
|
|
2
|
+
|
|
3
|
+
function mergeHeaders(base: HeadersInit, extra: HeadersInit) {
|
|
4
|
+
base = new Headers(base ?? {});
|
|
5
|
+
extra = new Headers(extra ?? {});
|
|
6
|
+
|
|
7
|
+
return new Headers({
|
|
8
|
+
...Object.fromEntries(base.entries()),
|
|
9
|
+
...Object.fromEntries(extra.entries()),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class OkResponse extends Response {
|
|
14
|
+
constructor(...[body, init]: ConstructorParameters<typeof Response>) {
|
|
15
|
+
super(body, {
|
|
16
|
+
...init,
|
|
17
|
+
status: 200,
|
|
18
|
+
statusText: "OK",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class MovedPermanentlyResponse extends Response {
|
|
24
|
+
constructor(
|
|
25
|
+
location: string,
|
|
26
|
+
init?: ConstructorParameters<typeof Response>[1]
|
|
27
|
+
) {
|
|
28
|
+
super(`Redirecting to ${location}`, {
|
|
29
|
+
...init,
|
|
30
|
+
status: 301,
|
|
31
|
+
statusText: "Moved Permanently",
|
|
32
|
+
headers: mergeHeaders(init?.headers, {
|
|
33
|
+
location,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class FoundResponse extends Response {
|
|
40
|
+
constructor(
|
|
41
|
+
location: string,
|
|
42
|
+
init?: ConstructorParameters<typeof Response>[1]
|
|
43
|
+
) {
|
|
44
|
+
super(`Redirecting to ${location}`, {
|
|
45
|
+
...init,
|
|
46
|
+
status: 302,
|
|
47
|
+
statusText: "Found",
|
|
48
|
+
headers: mergeHeaders(init?.headers, {
|
|
49
|
+
location,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class NotModifiedResponse extends Response {
|
|
56
|
+
constructor(...[_body, _init]: ConstructorParameters<typeof Response>) {
|
|
57
|
+
super(undefined, {
|
|
58
|
+
status: 304,
|
|
59
|
+
statusText: "Not Modified",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class PermanentRedirectResponse extends Response {
|
|
65
|
+
constructor(
|
|
66
|
+
location: string,
|
|
67
|
+
init?: ConstructorParameters<typeof Response>[1]
|
|
68
|
+
) {
|
|
69
|
+
super(undefined, {
|
|
70
|
+
...init,
|
|
71
|
+
status: 308,
|
|
72
|
+
statusText: "Permanent Redirect",
|
|
73
|
+
headers: mergeHeaders(init?.headers, {
|
|
74
|
+
location,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class NotFoundResponse extends Response {
|
|
81
|
+
constructor(...[body, init]: ConstructorParameters<typeof Response>) {
|
|
82
|
+
super(body, {
|
|
83
|
+
...init,
|
|
84
|
+
status: 404,
|
|
85
|
+
statusText: "Not Found",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class MethodNotAllowedResponse extends Response {
|
|
91
|
+
constructor(...[body, init]: ConstructorParameters<typeof Response>) {
|
|
92
|
+
super(body, {
|
|
93
|
+
...init,
|
|
94
|
+
status: 405,
|
|
95
|
+
statusText: "Method Not Allowed",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class NotAcceptableResponse extends Response {
|
|
101
|
+
constructor(...[body, init]: ConstructorParameters<typeof Response>) {
|
|
102
|
+
super(body, {
|
|
103
|
+
...init,
|
|
104
|
+
status: 406,
|
|
105
|
+
statusText: "Not Acceptable",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class InternalServerErrorResponse extends Response {
|
|
111
|
+
constructor(err: Error, init?: ConstructorParameters<typeof Response>[1]) {
|
|
112
|
+
let body: string | undefined = undefined;
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
if ((globalThis as any).DEBUG) {
|
|
115
|
+
body = `${err.message}\n\n${err.stack}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
super(body, {
|
|
119
|
+
...init,
|
|
120
|
+
status: 500,
|
|
121
|
+
statusText: "Internal Server Error",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class SeeOtherResponse extends Response {
|
|
127
|
+
constructor(
|
|
128
|
+
location: string,
|
|
129
|
+
init?: ConstructorParameters<typeof Response>[1]
|
|
130
|
+
) {
|
|
131
|
+
super(`Redirecting to ${location}`, {
|
|
132
|
+
...init,
|
|
133
|
+
status: 303,
|
|
134
|
+
statusText: "See Other",
|
|
135
|
+
headers: mergeHeaders(init?.headers, { location }),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class TemporaryRedirectResponse extends Response {
|
|
141
|
+
constructor(
|
|
142
|
+
location: string,
|
|
143
|
+
init?: ConstructorParameters<typeof Response>[1]
|
|
144
|
+
) {
|
|
145
|
+
super(`Redirecting to ${location}`, {
|
|
146
|
+
...init,
|
|
147
|
+
status: 307,
|
|
148
|
+
statusText: "Temporary Redirect",
|
|
149
|
+
headers: mergeHeaders(init?.headers, { location }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Taken from https://stackoverflow.com/a/3561711
|
|
2
|
+
// which is everything from the tc39 proposal, plus the following two characters: ^/
|
|
3
|
+
// It's also everything included in the URLPattern escape (https://wicg.github.io/urlpattern/#escape-a-regexp-string), plus the following: -
|
|
4
|
+
// As the answer says, there's no downside to escaping these extra characters, so better safe than sorry
|
|
5
|
+
const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
|
|
6
|
+
const escapeRegex = (str: string) => {
|
|
7
|
+
return str.replace(ESCAPE_REGEX_CHARACTERS, "\\$&");
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Placeholder names must begin with a colon, be alphanumeric and optionally contain underscores.
|
|
11
|
+
// e.g. :place_123_holder
|
|
12
|
+
const HOST_PLACEHOLDER_REGEX = /(?<=^https:\\\/\\\/[^/]*?):([^\\]+)(?=\\)/g;
|
|
13
|
+
const PLACEHOLDER_REGEX = /:(\w+)/g;
|
|
14
|
+
|
|
15
|
+
export type Replacements = Record<string, string>;
|
|
16
|
+
|
|
17
|
+
export type Removals = string[];
|
|
18
|
+
|
|
19
|
+
export const replacer = (str: string, replacements: Replacements) => {
|
|
20
|
+
for (const [replacement, value] of Object.entries(replacements)) {
|
|
21
|
+
str = str.replaceAll(`:${replacement}`, value);
|
|
22
|
+
}
|
|
23
|
+
return str;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const generateRulesMatcher = <T>(
|
|
27
|
+
rules?: Record<string, T>,
|
|
28
|
+
replacerFn: (match: T, replacements: Replacements) => T = (match) => match
|
|
29
|
+
) => {
|
|
30
|
+
if (!rules) return () => [];
|
|
31
|
+
|
|
32
|
+
const compiledRules = Object.entries(rules)
|
|
33
|
+
.map(([rule, match]) => {
|
|
34
|
+
const crossHost = rule.startsWith("https://");
|
|
35
|
+
|
|
36
|
+
// Create :splat capturer then escape.
|
|
37
|
+
rule = rule.split("*").map(escapeRegex).join("(?<splat>.*)");
|
|
38
|
+
|
|
39
|
+
// Create :placeholder capturers (already escaped).
|
|
40
|
+
// For placeholders in the host, we separate at forward slashes and periods.
|
|
41
|
+
// For placeholders in the path, we separate at forward slashes.
|
|
42
|
+
// This matches the behavior of URLPattern.
|
|
43
|
+
// e.g. https://:subdomain.domain/ -> https://(here).domain/
|
|
44
|
+
// e.g. /static/:file -> /static/(image.jpg)
|
|
45
|
+
// e.g. /blog/:post -> /blog/(an-exciting-post)
|
|
46
|
+
const host_matches = rule.matchAll(HOST_PLACEHOLDER_REGEX);
|
|
47
|
+
for (const host_match of host_matches) {
|
|
48
|
+
rule = rule.split(host_match[0]).join(`(?<${host_match[1]}>[^/.]+)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const path_matches = rule.matchAll(PLACEHOLDER_REGEX);
|
|
52
|
+
for (const path_match of path_matches) {
|
|
53
|
+
rule = rule.split(path_match[0]).join(`(?<${path_match[1]}>[^/]+)`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Wrap in line terminators to be safe.
|
|
57
|
+
rule = "^" + rule + "$";
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const regExp = new RegExp(rule);
|
|
61
|
+
return [{ crossHost, regExp }, match];
|
|
62
|
+
} catch {}
|
|
63
|
+
})
|
|
64
|
+
.filter((value) => value !== undefined) as [
|
|
65
|
+
{ crossHost: boolean; regExp: RegExp },
|
|
66
|
+
T
|
|
67
|
+
][];
|
|
68
|
+
|
|
69
|
+
return ({ request }: { request: Request }) => {
|
|
70
|
+
const { pathname, host } = new URL(request.url);
|
|
71
|
+
|
|
72
|
+
return compiledRules
|
|
73
|
+
.map(([{ crossHost, regExp }, match]) => {
|
|
74
|
+
const test = crossHost ? `https://${host}${pathname}` : pathname;
|
|
75
|
+
const result = regExp.exec(test);
|
|
76
|
+
if (result) {
|
|
77
|
+
return replacerFn(match, result.groups || {});
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.filter((value) => value !== undefined) as T[];
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PolyfilledRuntimeEnvironment } from "./types";
|
|
2
|
+
|
|
3
|
+
export const polyfill = (
|
|
4
|
+
environment: Record<keyof PolyfilledRuntimeEnvironment, unknown>
|
|
5
|
+
) => {
|
|
6
|
+
Object.entries(environment).map(([name, value]) => {
|
|
7
|
+
Object.defineProperty(globalThis, name, {
|
|
8
|
+
value,
|
|
9
|
+
configurable: true,
|
|
10
|
+
enumerable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetch as miniflareFetch,
|
|
3
|
+
Headers as MiniflareHeaders,
|
|
4
|
+
Request as MiniflareRequest,
|
|
5
|
+
Response as MiniflareResponse,
|
|
6
|
+
} from "@miniflare/core";
|
|
7
|
+
import { polyfill } from ".";
|
|
8
|
+
|
|
9
|
+
polyfill({
|
|
10
|
+
fetch: miniflareFetch,
|
|
11
|
+
Headers: MiniflareHeaders,
|
|
12
|
+
Request: MiniflareRequest,
|
|
13
|
+
Response: MiniflareResponse,
|
|
14
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Headers as MiniflareHeaders,
|
|
3
|
+
Request as MiniflareRequest,
|
|
4
|
+
Response as MiniflareResponse,
|
|
5
|
+
} from "@miniflare/core";
|
|
6
|
+
import { HTMLRewriter as MiniflareHTMLRewriter } from "@miniflare/html-rewriter";
|
|
7
|
+
import type { CacheInterface as MiniflareCacheInterface } from "@miniflare/cache";
|
|
8
|
+
import type { fetch as miniflareFetch } from "@miniflare/core";
|
|
9
|
+
import type { ReadableStream as SimilarReadableStream } from "stream/web";
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
const fetch: typeof miniflareFetch;
|
|
13
|
+
class Headers extends MiniflareHeaders {}
|
|
14
|
+
class Request extends MiniflareRequest {}
|
|
15
|
+
class Response extends MiniflareResponse {}
|
|
16
|
+
|
|
17
|
+
type CacheInterface = Omit<MiniflareCacheInterface, "match"> & {
|
|
18
|
+
match(
|
|
19
|
+
...args: Parameters<MiniflareCacheInterface["match"]>
|
|
20
|
+
): Promise<Response | undefined>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class CacheStorage {
|
|
24
|
+
get default(): CacheInterface;
|
|
25
|
+
open(cacheName: string): Promise<CacheInterface>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class HTMLRewriter extends MiniflareHTMLRewriter {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
transform(response: Response): Response;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ReadableStream = SimilarReadableStream;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PolyfilledRuntimeEnvironment = {
|
|
38
|
+
fetch: typeof fetch;
|
|
39
|
+
Headers: typeof Headers;
|
|
40
|
+
Request: typeof Request;
|
|
41
|
+
Response: typeof Response;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export { fetch, Headers, Request, Response };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const REDIRECTS_VERSION = 1;
|
|
2
|
+
export const HEADERS_VERSION = 2;
|
|
3
|
+
export const ANALYTICS_VERSION = 1;
|
|
4
|
+
export const ROUTES_JSON_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export const PERMITTED_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
7
|
+
export const HEADER_SEPARATOR = ":";
|
|
8
|
+
export const MAX_LINE_LENGTH = 2000;
|
|
9
|
+
export const MAX_HEADER_RULES = 100;
|
|
10
|
+
export const MAX_DYNAMIC_REDIRECT_RULES = 100;
|
|
11
|
+
export const MAX_STATIC_REDIRECT_RULES = 2000;
|
|
12
|
+
export const UNSET_OPERATOR = "! ";
|
|
13
|
+
|
|
14
|
+
export const SPLAT_REGEX = /\*/g;
|
|
15
|
+
export const PLACEHOLDER_REGEX = /:\w+/g;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANALYTICS_VERSION,
|
|
3
|
+
REDIRECTS_VERSION,
|
|
4
|
+
HEADERS_VERSION,
|
|
5
|
+
SPLAT_REGEX,
|
|
6
|
+
PLACEHOLDER_REGEX,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
import type {
|
|
9
|
+
Metadata,
|
|
10
|
+
MetadataRedirects,
|
|
11
|
+
MetadataHeaders,
|
|
12
|
+
ParsedRedirects,
|
|
13
|
+
ParsedHeaders,
|
|
14
|
+
Logger,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
export function createMetadataObject({
|
|
18
|
+
redirects,
|
|
19
|
+
headers,
|
|
20
|
+
webAnalyticsToken,
|
|
21
|
+
deploymentId,
|
|
22
|
+
logger = (_message: string) => {},
|
|
23
|
+
}: {
|
|
24
|
+
redirects?: ParsedRedirects;
|
|
25
|
+
headers?: ParsedHeaders;
|
|
26
|
+
webAnalyticsToken?: string;
|
|
27
|
+
deploymentId?: string;
|
|
28
|
+
logger?: Logger;
|
|
29
|
+
}): Metadata {
|
|
30
|
+
return {
|
|
31
|
+
...constructRedirects({ redirects, logger }),
|
|
32
|
+
...constructHeaders({ headers, logger }),
|
|
33
|
+
...constructWebAnalytics({ webAnalyticsToken, logger }),
|
|
34
|
+
deploymentId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function constructRedirects({
|
|
39
|
+
redirects,
|
|
40
|
+
logger,
|
|
41
|
+
}: {
|
|
42
|
+
redirects?: ParsedRedirects;
|
|
43
|
+
logger: Logger;
|
|
44
|
+
}): Metadata {
|
|
45
|
+
if (!redirects) return {};
|
|
46
|
+
|
|
47
|
+
const num_valid = redirects.rules.length;
|
|
48
|
+
const num_invalid = redirects.invalid.length;
|
|
49
|
+
|
|
50
|
+
logger(
|
|
51
|
+
`Parsed ${num_valid} valid redirect rule${num_valid === 1 ? "" : "s"}.`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (num_invalid > 0) {
|
|
55
|
+
logger(`Found invalid redirect lines:`);
|
|
56
|
+
for (const { line, lineNumber, message } of redirects.invalid) {
|
|
57
|
+
if (line) logger(` - ${lineNumber ? `#${lineNumber}: ` : ""}${line}`);
|
|
58
|
+
logger(` ${message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Better to return no Redirects object at all than one with empty rules */
|
|
63
|
+
if (num_valid === 0) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const staticRedirects: MetadataRedirects = {};
|
|
68
|
+
const dynamicRedirects: MetadataRedirects = {};
|
|
69
|
+
let canCreateStaticRule = true;
|
|
70
|
+
for (const rule of redirects.rules) {
|
|
71
|
+
if (!rule.from.match(SPLAT_REGEX) && !rule.from.match(PLACEHOLDER_REGEX)) {
|
|
72
|
+
if (canCreateStaticRule) {
|
|
73
|
+
staticRedirects[rule.from] = { status: rule.status, to: rule.to };
|
|
74
|
+
continue;
|
|
75
|
+
} else {
|
|
76
|
+
logger(
|
|
77
|
+
`Info: the redirect rule ${rule.from} → ${rule.status} ${rule.to} could be made more performant by bringing it above any lines with splats or placeholders.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
dynamicRedirects[rule.from] = { status: rule.status, to: rule.to };
|
|
83
|
+
canCreateStaticRule = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
redirects: {
|
|
88
|
+
version: REDIRECTS_VERSION,
|
|
89
|
+
staticRules: staticRedirects,
|
|
90
|
+
rules: dynamicRedirects,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function constructHeaders({
|
|
96
|
+
headers,
|
|
97
|
+
logger,
|
|
98
|
+
}: {
|
|
99
|
+
headers?: ParsedHeaders;
|
|
100
|
+
logger: Logger;
|
|
101
|
+
}): Metadata {
|
|
102
|
+
if (!headers) return {};
|
|
103
|
+
|
|
104
|
+
const num_valid = headers.rules.length;
|
|
105
|
+
const num_invalid = headers.invalid.length;
|
|
106
|
+
|
|
107
|
+
logger(`Parsed ${num_valid} valid header rule${num_valid === 1 ? "" : "s"}.`);
|
|
108
|
+
|
|
109
|
+
if (num_invalid > 0) {
|
|
110
|
+
logger(`Found invalid header lines:`);
|
|
111
|
+
for (const { line, lineNumber, message } of headers.invalid) {
|
|
112
|
+
if (line) logger(` - ${lineNumber ? `#${lineNumber}: ` : ""} ${line}`);
|
|
113
|
+
logger(` ${message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Better to return no Headers object at all than one with empty rules */
|
|
118
|
+
if (num_valid === 0) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rules: MetadataHeaders = {};
|
|
123
|
+
for (const rule of headers.rules) {
|
|
124
|
+
rules[rule.path] = {};
|
|
125
|
+
|
|
126
|
+
if (Object.keys(rule.headers).length) {
|
|
127
|
+
rules[rule.path].set = rule.headers;
|
|
128
|
+
}
|
|
129
|
+
if (rule.unsetHeaders.length) {
|
|
130
|
+
rules[rule.path].unset = rule.unsetHeaders;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
headers: {
|
|
136
|
+
version: HEADERS_VERSION,
|
|
137
|
+
rules,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function constructWebAnalytics({
|
|
143
|
+
webAnalyticsToken,
|
|
144
|
+
}: {
|
|
145
|
+
webAnalyticsToken?: string;
|
|
146
|
+
logger: Logger;
|
|
147
|
+
}) {
|
|
148
|
+
if (!webAnalyticsToken) return {};
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
analytics: {
|
|
152
|
+
version: ANALYTICS_VERSION,
|
|
153
|
+
token: webAnalyticsToken,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_LINE_LENGTH,
|
|
3
|
+
MAX_HEADER_RULES,
|
|
4
|
+
HEADER_SEPARATOR,
|
|
5
|
+
UNSET_OPERATOR,
|
|
6
|
+
} from "./constants";
|
|
7
|
+
import { validateUrl } from "./validateURL";
|
|
8
|
+
import type { InvalidHeadersRule, ParsedHeaders, HeadersRule } from "./types";
|
|
9
|
+
|
|
10
|
+
// Not strictly necessary to check for all protocols-like beginnings, since _technically_ that could be a legit header (e.g. name=http, value=://I'm a value).
|
|
11
|
+
// But we're checking here since some people might be caught out and it'll help 99.9% of people who get it wrong.
|
|
12
|
+
// We do the proper validation in `validateUrl` anyway :)
|
|
13
|
+
const LINE_IS_PROBABLY_A_PATH = new RegExp(/^([^\s]+:\/\/|^\/)/);
|
|
14
|
+
|
|
15
|
+
export function parseHeaders(input: string): ParsedHeaders {
|
|
16
|
+
const lines = input.split("\n");
|
|
17
|
+
const rules: HeadersRule[] = [];
|
|
18
|
+
const invalid: InvalidHeadersRule[] = [];
|
|
19
|
+
|
|
20
|
+
let rule: (HeadersRule & { line: string }) | undefined = undefined;
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i].trim();
|
|
24
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
25
|
+
|
|
26
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
27
|
+
invalid.push({
|
|
28
|
+
message: `Ignoring line ${
|
|
29
|
+
i + 1
|
|
30
|
+
} as it exceeds the maximum allowed length of ${MAX_LINE_LENGTH}.`,
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (LINE_IS_PROBABLY_A_PATH.test(line)) {
|
|
36
|
+
if (rules.length >= MAX_HEADER_RULES) {
|
|
37
|
+
invalid.push({
|
|
38
|
+
message: `Maximum number of rules supported is ${MAX_HEADER_RULES}. Skipping remaining ${
|
|
39
|
+
lines.length - i
|
|
40
|
+
} lines of file.`,
|
|
41
|
+
});
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (rule) {
|
|
46
|
+
if (isValidRule(rule)) {
|
|
47
|
+
rules.push({
|
|
48
|
+
path: rule.path,
|
|
49
|
+
headers: rule.headers,
|
|
50
|
+
unsetHeaders: rule.unsetHeaders,
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
invalid.push({
|
|
54
|
+
line: rule.line,
|
|
55
|
+
lineNumber: i + 1,
|
|
56
|
+
message: "No headers specified",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [path, pathError] = validateUrl(line);
|
|
62
|
+
if (pathError) {
|
|
63
|
+
invalid.push({
|
|
64
|
+
line,
|
|
65
|
+
lineNumber: i + 1,
|
|
66
|
+
message: pathError,
|
|
67
|
+
});
|
|
68
|
+
rule = undefined;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
rule = {
|
|
73
|
+
path: path as string,
|
|
74
|
+
line,
|
|
75
|
+
headers: {},
|
|
76
|
+
unsetHeaders: [],
|
|
77
|
+
};
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!line.includes(HEADER_SEPARATOR)) {
|
|
82
|
+
if (!rule) {
|
|
83
|
+
invalid.push({
|
|
84
|
+
line,
|
|
85
|
+
lineNumber: i + 1,
|
|
86
|
+
message: "Expected a path beginning with at least one forward-slash",
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
if (line.trim().startsWith(UNSET_OPERATOR)) {
|
|
90
|
+
rule.unsetHeaders.push(line.trim().replace(UNSET_OPERATOR, ""));
|
|
91
|
+
} else {
|
|
92
|
+
invalid.push({
|
|
93
|
+
line,
|
|
94
|
+
lineNumber: i + 1,
|
|
95
|
+
message:
|
|
96
|
+
"Expected a colon-separated header pair (e.g. name: value)",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const [rawName, ...rawValue] = line.split(HEADER_SEPARATOR);
|
|
104
|
+
const name = rawName.trim().toLowerCase();
|
|
105
|
+
|
|
106
|
+
if (name.includes(" ")) {
|
|
107
|
+
invalid.push({
|
|
108
|
+
line,
|
|
109
|
+
lineNumber: i + 1,
|
|
110
|
+
message: "Header name cannot include spaces",
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const value = rawValue.join(HEADER_SEPARATOR).trim();
|
|
116
|
+
|
|
117
|
+
if (name === "") {
|
|
118
|
+
invalid.push({
|
|
119
|
+
line,
|
|
120
|
+
lineNumber: i + 1,
|
|
121
|
+
message: "No header name specified",
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (value === "") {
|
|
127
|
+
invalid.push({
|
|
128
|
+
line,
|
|
129
|
+
lineNumber: i + 1,
|
|
130
|
+
message: "No header value specified",
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!rule) {
|
|
136
|
+
invalid.push({
|
|
137
|
+
line,
|
|
138
|
+
lineNumber: i + 1,
|
|
139
|
+
message: `Path should come before header (${name}: ${value})`,
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const existingValues = rule.headers[name];
|
|
145
|
+
rule.headers[name] = existingValues ? `${existingValues}, ${value}` : value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (rule) {
|
|
149
|
+
if (isValidRule(rule)) {
|
|
150
|
+
rules.push({
|
|
151
|
+
path: rule.path,
|
|
152
|
+
headers: rule.headers,
|
|
153
|
+
unsetHeaders: rule.unsetHeaders,
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
invalid.push({ line: rule.line, message: "No headers specified" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
rules,
|
|
162
|
+
invalid,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isValidRule(rule: HeadersRule) {
|
|
167
|
+
return Object.keys(rule.headers).length > 0 || rule.unsetHeaders.length > 0;
|
|
168
|
+
}
|