70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
222 lines
5.3 KiB
TypeScript
222 lines
5.3 KiB
TypeScript
import {
|
|
HEADER_SEPARATOR,
|
|
MAX_HEADER_RULES,
|
|
MAX_LINE_LENGTH,
|
|
SPLAT_REGEX,
|
|
UNSET_OPERATOR,
|
|
} from "./constants";
|
|
import { validateUrl } from "./validateURL";
|
|
import type { HeadersRule, InvalidHeadersRule, ParsedHeaders } from "./types";
|
|
|
|
// 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).
|
|
// But we're checking here since some people might be caught out and it'll help 99.9% of people who get it wrong.
|
|
// We do the proper validation in `validateUrl` anyway :)
|
|
const LINE_IS_PROBABLY_A_PATH = new RegExp(/^([^\s]+:\/\/|^\/)/);
|
|
|
|
export function parseHeaders(
|
|
input: string,
|
|
{
|
|
maxRules = MAX_HEADER_RULES,
|
|
maxLineLength = MAX_LINE_LENGTH,
|
|
}: { maxRules?: number; maxLineLength?: number } = {}
|
|
): ParsedHeaders {
|
|
const lines = input.split("\n");
|
|
const rules: HeadersRule[] = [];
|
|
const invalid: InvalidHeadersRule[] = [];
|
|
|
|
let rule: (HeadersRule & { line: string }) | undefined = undefined;
|
|
// When a path line is rejected (invalid URL, multiple wildcards, etc.),
|
|
// we silently skip subsequent header lines until the next path line
|
|
// rather than emitting confusing "Path should come before header" errors.
|
|
let skipUntilNextPath = false;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = (lines[i] || "").trim();
|
|
if (line.length === 0 || line.startsWith("#")) {
|
|
continue;
|
|
}
|
|
|
|
if (line.length > maxLineLength) {
|
|
invalid.push({
|
|
message: `Ignoring line ${
|
|
i + 1
|
|
} as it exceeds the maximum allowed length of ${maxLineLength}.`,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (LINE_IS_PROBABLY_A_PATH.test(line)) {
|
|
skipUntilNextPath = false;
|
|
|
|
if (rules.length >= maxRules) {
|
|
invalid.push({
|
|
message: `Maximum number of rules supported is ${maxRules}. Skipping remaining ${
|
|
lines.length - i
|
|
} lines of file.`,
|
|
});
|
|
break;
|
|
}
|
|
|
|
if (rule) {
|
|
if (isValidRule(rule)) {
|
|
rules.push({
|
|
path: rule.path,
|
|
headers: rule.headers,
|
|
unsetHeaders: rule.unsetHeaders,
|
|
});
|
|
} else {
|
|
invalid.push({
|
|
line: rule.line,
|
|
lineNumber: i + 1,
|
|
message: "No headers specified",
|
|
});
|
|
}
|
|
}
|
|
|
|
const [path, pathError] = validateUrl(line, false, true);
|
|
if (pathError) {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: pathError,
|
|
});
|
|
rule = undefined;
|
|
skipUntilNextPath = true;
|
|
continue;
|
|
}
|
|
|
|
const wildcardError = validateNoMultipleWildcards(path as string);
|
|
if (wildcardError) {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: wildcardError,
|
|
});
|
|
rule = undefined;
|
|
skipUntilNextPath = true;
|
|
continue;
|
|
}
|
|
|
|
rule = {
|
|
path: path as string,
|
|
line,
|
|
headers: {},
|
|
unsetHeaders: [],
|
|
};
|
|
continue;
|
|
}
|
|
|
|
if (skipUntilNextPath) {
|
|
continue;
|
|
}
|
|
|
|
if (!line.includes(HEADER_SEPARATOR)) {
|
|
if (!rule) {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: "Expected a path beginning with at least one forward-slash",
|
|
});
|
|
} else {
|
|
if (line.trim().startsWith(UNSET_OPERATOR)) {
|
|
rule.unsetHeaders.push(line.trim().replace(UNSET_OPERATOR, ""));
|
|
} else {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message:
|
|
"Expected a colon-separated header pair (e.g. name: value)",
|
|
});
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const [rawName, ...rawValue] = line.split(HEADER_SEPARATOR);
|
|
const name = (rawName || "").trim().toLowerCase();
|
|
|
|
if (name.includes(" ")) {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: "Header name cannot include spaces",
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const value = rawValue.join(HEADER_SEPARATOR).trim();
|
|
|
|
if (name === "") {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: "No header name specified",
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (value === "") {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: "No header value specified",
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (!rule) {
|
|
invalid.push({
|
|
line,
|
|
lineNumber: i + 1,
|
|
message: `Path should come before header (${name}: ${value})`,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const existingValues = rule.headers[name];
|
|
rule.headers[name] = existingValues ? `${existingValues}, ${value}` : value;
|
|
}
|
|
|
|
if (rule) {
|
|
if (isValidRule(rule)) {
|
|
rules.push({
|
|
path: rule.path,
|
|
headers: rule.headers,
|
|
unsetHeaders: rule.unsetHeaders,
|
|
});
|
|
} else {
|
|
invalid.push({ line: rule.line, message: "No headers specified" });
|
|
}
|
|
}
|
|
|
|
return {
|
|
rules,
|
|
invalid,
|
|
};
|
|
}
|
|
|
|
function isValidRule(rule: HeadersRule) {
|
|
return Object.keys(rule.headers).length > 0 || rule.unsetHeaders.length > 0;
|
|
}
|
|
|
|
/**
|
|
* At runtime, `*` wildcards are converted to `:splat` placeholders. This means
|
|
* a path with multiple wildcards, or a wildcard combined with an explicit
|
|
* `:splat` placeholder, would result in duplicate `:splat` parameters which is
|
|
* unsupported.
|
|
*/
|
|
function validateNoMultipleWildcards(path: string): string | undefined {
|
|
const wildcardCount = (path.match(SPLAT_REGEX) ?? []).length;
|
|
const hasSplatPlaceholder = /:splat(?!\w)/.test(path);
|
|
|
|
if (wildcardCount > 1) {
|
|
return `Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping ${path}.`;
|
|
}
|
|
|
|
if (wildcardCount > 0 && hasSplatPlaceholder) {
|
|
return `Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping ${path}.`;
|
|
}
|
|
|
|
return undefined;
|
|
}
|