1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3import { test } from "vitest";
4import { parseHeaders } from "../configuration/parseHeaders.ts";
5
6test("parseHeaders should reject malformed initial lines", ({ expect }) => {
7 const input = `
8 # A single token before a path
9 c
10 # A header before a path
11 Access-Control-Allow-Origin: *
12 `;
13 const result = parseHeaders(input);
14 expect(result).toEqual({
15 rules: [],
16 invalid: [
17 {
18 line: "c",
19 lineNumber: 3,
20 message: "Expected a path beginning with at least one forward-slash",
21 },
22 {
23 line: "Access-Control-Allow-Origin: *",
24 lineNumber: 5,
25 message:
26 "Path should come before header (access-control-allow-origin: *)",
27 },
28 ],
29 });
30});
31
32test("parseHeaders should reject invalid headers", ({ expect }) => {
33 const input = `
34 # Valid header sails through
35 /a
36 Name: Value
37 #
38 /b
39 I'm invalid!
40 !x-content-type
41 But: I'm okay!
42 ! Content-Type: application/json
43 `;
44 const result = parseHeaders(input);
45 expect(result).toEqual({
46 rules: [
47 { path: "/a", headers: { name: "Value" }, unsetHeaders: [] },
48 { path: "/b", headers: { but: "I'm okay!" }, unsetHeaders: [] },
49 ],
50 invalid: [
51 {
52 line: `I'm invalid!`,
53 lineNumber: 7,
54 message: "Expected a colon-separated header pair (e.g. name: value)",
55 },
56 {
57 line: "!x-content-type",
58 lineNumber: 8,
59 message: "Expected a colon-separated header pair (e.g. name: value)",
60 },
61 {
62 line: `! Content-Type: application/json`,
63 lineNumber: 10,
64 message: "Header name cannot include spaces",
65 },
66 ],
67 });
68});
69
70test("parseHeaders should reject lines longer than 2000 chars", ({
71 expect,
72}) => {
73 const huge_line = `${Array(1001).fill("a").join("")}: ${Array(1001).fill("b").join("")}`;
74 const input = `
75 # Valid entry
76 /a
77 Name: Value
78 # Jumbo comment line OK, ignored as normal
79 ${Array(1001).fill("#").join("")}
80 # Huge path names rejected
81 /b
82 Name: Value
83 ${huge_line}
84 `;
85 const result = parseHeaders(input);
86 expect(result).toEqual({
87 rules: [
88 { path: "/a", headers: { name: "Value" }, unsetHeaders: [] },
89 { path: "/b", headers: { name: "Value" }, unsetHeaders: [] },
90 ],
91 invalid: [
92 {
93 message: `Ignoring line 10 as it exceeds the maximum allowed length of 2000.`,
94 },
95 ],
96 });
97});
98
99test("parseHeaders should reject any rules after the first 100", ({
100 expect,
101}) => {
102 const input = `
103 # COMMENTS DON'T COUNT TOWARDS TOTAL VALID RULES
104 ${Array(150)
105 .fill(undefined)
106 .map((_, i) => `/a/${i}\nx-index: ${i}`)
107 .join("\n")}
108 # BUT DO GET COUNTED AS TOTAL LINES SKIPPED
109 `;
110
111 expect(parseHeaders(input)).toEqual({
112 rules: Array(101)
113 .fill(undefined)
114 .map((_, i) => ({
115 path: "/a/" + i,
116 headers: { "x-index": `${i}` },
117 unsetHeaders: [],
118 })),
119 invalid: [
120 {
121 message: `Maximum number of rules supported is 100. Skipping remaining 100 lines of file.`,
122 },
123 ],
124 });
125});
126
127test("parseHeaders should reject paths with multiple wildcards", ({
128 expect,
129}) => {
130 const input = `
131 # Multiple wildcards in an absolute URL
132 https://*.pages.dev/*
133 x-custom: value
134 # Multiple wildcards in a relative path
135 /blog/*/posts/*
136 x-custom: value
137 # Single wildcard is fine
138 https://*.pages.dev/
139 x-custom: value
140 /blog/*
141 x-custom: value
142`;
143 const result = parseHeaders(input);
144 expect(result).toEqual({
145 rules: [
146 {
147 path: "https://*.pages.dev/",
148 headers: { "x-custom": "value" },
149 unsetHeaders: [],
150 },
151 {
152 path: "/blog/*",
153 headers: { "x-custom": "value" },
154 unsetHeaders: [],
155 },
156 ],
157 invalid: [
158 {
159 line: "https://*.pages.dev/*",
160 lineNumber: 3,
161 message:
162 "Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping https://*.pages.dev/*.",
163 },
164 {
165 line: "/blog/*/posts/*",
166 lineNumber: 6,
167 message:
168 "Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping /blog/*/posts/*.",
169 },
170 ],
171 });
172});
173
174test("parseHeaders should reject paths combining wildcard with :splat placeholder", ({
175 expect,
176}) => {
177 const input = `
178 # Wildcard + :splat in an absolute URL
179 https://*.pages.dev/:splat
180 x-custom: value
181 # Wildcard + :splat in a relative path
182 /blog/*/:splat
183 x-custom: value
184 # Just :splat without wildcard is fine
185 /blog/:splat
186 x-custom: value
187`;
188 const result = parseHeaders(input);
189 expect(result).toEqual({
190 rules: [
191 {
192 path: "/blog/:splat",
193 headers: { "x-custom": "value" },
194 unsetHeaders: [],
195 },
196 ],
197 invalid: [
198 {
199 line: "https://*.pages.dev/:splat",
200 lineNumber: 3,
201 message:
202 "Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping https://*.pages.dev/:splat.",
203 },
204 {
205 line: "/blog/*/:splat",
206 lineNumber: 6,
207 message:
208 "Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping /blog/*/:splat.",
209 },
210 ],
211 });
212});
213
214test("parseHeaders should reject malformed URLs", ({ expect }) => {
215 const input = `
216 # Spaces should be URI encoded
217 /some page with spaces
218 valid: yup
219 # OK with URL escaped encoding
220 /some%20page
221 valid: yup
222 # Unescaped URLs are handled OK by Deno, so escape & pass them through
223 /://so;\`me
224 valid: yup
225 /nons:/&@%+~{}ense
226 valid: yup
227 # Absolute URLs with a non-https protocol should be rejected
228 https://yeah.com
229 valid: yup
230 http://nah.com/blog
231 invalid: things
232 # Absolute URLs with a port should be rejected
233 https://nah.com:8080
234 invalid: also
235 https://nah.com:8080/blog
236 invalid: 2
237 //yeah.com/blog
238 valid: things
239 /yeah
240 valid: yup
241 /yeah.com
242 valid: yup
243 # Anything standalone is interpreted as a invalid header pair
244 nah.com
245 :
246 test:
247`;
248 const result = parseHeaders(input);
249 expect(result).toEqual({
250 invalid: [
251 {
252 line: "http://nah.com/blog",
253 lineNumber: 16,
254 message:
255 'URLs should either be relative (e.g. begin with a forward-slash), or use HTTPS (e.g. begin with "https://").',
256 },
257 {
258 line: "https://nah.com:8080",
259 lineNumber: 19,
260 message:
261 "Specifying ports is not supported. Skipping absolute URL https://nah.com:8080.",
262 },
263 {
264 line: "https://nah.com:8080/blog",
265 lineNumber: 21,
266 message:
267 "Specifying ports is not supported. Skipping absolute URL https://nah.com:8080/blog.",
268 },
269 {
270 line: "nah.com",
271 lineNumber: 30,
272 message: "Expected a colon-separated header pair (e.g. name: value)",
273 },
274 { line: ":", lineNumber: 31, message: "No header name specified" },
275 { line: "test:", lineNumber: 32, message: "No header value specified" },
276 ],
277 rules: [
278 {
279 path: "/some%20page%20with%20spaces",
280 headers: { valid: "yup" },
281 unsetHeaders: [],
282 },
283 { path: "/some%20page", headers: { valid: "yup" }, unsetHeaders: [] },
284 { path: "/://so;%60me", headers: { valid: "yup" }, unsetHeaders: [] },
285 {
286 path: "/nons:/&@%+~%7B%7Dense",
287 headers: { valid: "yup" },
288 unsetHeaders: [],
289 },
290 {
291 path: "https://yeah.com/",
292 headers: { valid: "yup" },
293 unsetHeaders: [],
294 },
295 {
296 path: "//yeah.com/blog",
297 headers: { valid: "things" },
298 unsetHeaders: [],
299 },
300 { path: "/yeah", headers: { valid: "yup" }, unsetHeaders: [] },
301 { path: "/yeah.com", headers: { valid: "yup" }, unsetHeaders: [] },
302 ],
303 });
304});