@fishawack/lab-env 5.2.0 → 5.3.0-beta.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## Changelog
2
2
 
3
+ ### 5.3.0-beta.1 (2026-02-04)
4
+
5
+ #### Features
6
+
7
+ * can now customize headers for static provisions ([a3bc776](https://bitbucket.org/fishawackdigital/lab-env/commits/a3bc776c6acca3555de4ef5667ffdf9f8b37a518))
8
+ * can now specify redirects for use in cloudfront functions ([67706d7](https://bitbucket.org/fishawackdigital/lab-env/commits/67706d76300fc6743f7a5ceb9bf31748cf8acb31))
9
+
10
+ #### Bug Fixes
11
+
12
+ * deprov now correctly removes the response function for static sites ([69fbc2b](https://bitbucket.org/fishawackdigital/lab-env/commits/69fbc2b8f60898d3d33416328752897dff6f752b))
13
+ * prompt if redirects are needed first ([e087bf0](https://bitbucket.org/fishawackdigital/lab-env/commits/e087bf0f6d86c543f31d9f0c401793222f8f0d9a))
14
+ * set aws client on key command ([3b15512](https://bitbucket.org/fishawackdigital/lab-env/commits/3b15512b168d4ba35871047fd0ea9230cf394804))
15
+
16
+ ### 5.2.1-beta.1 (2026-01-26)
17
+
18
+ #### Bug Fixes
19
+
20
+ * issue with lab-env calling lab-env and concatanating repo env variable ([03313c3](https://bitbucket.org/fishawackdigital/lab-env/commits/03313c3c43c793222620170c381af05ec4b1c348))
21
+ * use correct prod base image on python images ([db0ace4](https://bitbucket.org/fishawackdigital/lab-env/commits/db0ace419e8c635c727aa33330a3156c0956af0b))
22
+
3
23
  ### 5.2.0 (2026-01-17)
4
24
 
5
25
  #### Features
@@ -1,6 +1,9 @@
1
1
  const _ = require("../../../globals.js");
2
2
  const inquirer = require("inquirer");
3
3
  const aws = require("../services/aws/index.js");
4
+ const {
5
+ setAWSClientDefaults,
6
+ } = require("../../../commands/create/services/aws/misc.js");
4
7
 
5
8
  module.exports = [
6
9
  "dekey",
@@ -63,6 +66,8 @@ module.exports = [
63
66
  for (let i = 0; i < clients.length; i++) {
64
67
  let client = clients[i];
65
68
 
69
+ setAWSClientDefaults(client);
70
+
66
71
  for (let j = 0; j < users.length; j++) {
67
72
  let user = users[j];
68
73
 
@@ -7,6 +7,9 @@ const htmlEmail = require("fs").readFileSync(
7
7
  `${__dirname}/../templates/credentials.html`,
8
8
  { encoding: "utf8" },
9
9
  );
10
+ const {
11
+ setAWSClientDefaults,
12
+ } = require("../../../commands/create/services/aws/misc.js");
10
13
 
11
14
  module.exports = [
12
15
  "key",
@@ -83,6 +86,8 @@ module.exports = [
83
86
  for (let i = 0; i < clients.length; i++) {
84
87
  let client = clients[i];
85
88
 
89
+ setAWSClientDefaults(client);
90
+
86
91
  for (let j = 0; j < users.length; j++) {
87
92
  let user = users[j];
88
93
 
@@ -32,12 +32,169 @@ module.exports = [
32
32
  },
33
33
  ]);
34
34
 
35
+ // Check for existing redirects in config
36
+ let redirects = _.coreConfig?.attributes?.deploy?.redirects || [];
37
+
35
38
  if (!answer.check) {
36
39
  process.exit(1);
37
40
  }
38
41
 
39
42
  let answers = await inquirer.prompt([stack]);
40
43
 
44
+ // Prompt for redirects if static stack and no existing redirects
45
+ if (answers.stack === "static" && redirects.length === 0) {
46
+ let wantsRedirects = await inquirer.prompt([
47
+ {
48
+ type: "confirm",
49
+ name: "setup",
50
+ message: "Setup redirects?",
51
+ default: false,
52
+ },
53
+ ]);
54
+
55
+ if (wantsRedirects.setup) {
56
+ let addRedirect = { continue: true };
57
+
58
+ while (addRedirect.continue) {
59
+ addRedirect = await inquirer.prompt([
60
+ {
61
+ type: "input",
62
+ name: "src",
63
+ message:
64
+ "Redirect source path (e.g., /old-path or /news/*)",
65
+ validate: (input) => {
66
+ if (!input || !input.startsWith("/")) {
67
+ return "Source must start with /";
68
+ }
69
+ return true;
70
+ },
71
+ },
72
+ {
73
+ type: "input",
74
+ name: "dest",
75
+ message:
76
+ "Redirect destination (e.g., /new-path or https://example.com/news/*)",
77
+ validate: (input) => !!input.length,
78
+ },
79
+ {
80
+ type: "confirm",
81
+ name: "continue",
82
+ message: "Add another redirect?",
83
+ default: true,
84
+ },
85
+ ]);
86
+
87
+ if (addRedirect.src && addRedirect.dest) {
88
+ redirects.push({
89
+ src: addRedirect.src,
90
+ dest: addRedirect.dest,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ // Prompt for custom headers if static stack and no existing headers
98
+ let headers = _.coreConfig?.attributes?.deploy?.headers || [];
99
+
100
+ if (answers.stack === "static" && headers.length === 0) {
101
+ let customHeaders = await inquirer.prompt([
102
+ {
103
+ type: "confirm",
104
+ name: "customize",
105
+ message: "Customize response headers?",
106
+ default: false,
107
+ },
108
+ ]);
109
+
110
+ if (customHeaders.customize) {
111
+ // Prompt for standard headers with defaults
112
+ let standardHeaders = await inquirer.prompt([
113
+ {
114
+ type: "input",
115
+ name: "strict-transport-security",
116
+ message: "strict-transport-security header value:",
117
+ default: "max-age=31536000; includeSubDomains",
118
+ validate: (input) => !!input.length,
119
+ },
120
+ {
121
+ type: "input",
122
+ name: "content-security-policy",
123
+ message: "content-security-policy header value:",
124
+ default:
125
+ "default-src 'self' https: data: 'unsafe-inline';",
126
+ validate: (input) => !!input.length,
127
+ },
128
+ {
129
+ type: "input",
130
+ name: "x-content-type-options",
131
+ message: "x-content-type-options header value:",
132
+ default: "nosniff",
133
+ validate: (input) => !!input.length,
134
+ },
135
+ {
136
+ type: "input",
137
+ name: "x-frame-options",
138
+ message: "x-frame-options header value:",
139
+ default: "sameorigin",
140
+ validate: (input) => !!input.length,
141
+ },
142
+ ]);
143
+
144
+ // Add standard headers to array
145
+ Object.keys(standardHeaders).forEach((name) => {
146
+ headers.push({
147
+ name: name,
148
+ value: standardHeaders[name],
149
+ });
150
+ });
151
+
152
+ // Prompt for additional custom headers
153
+ let wantsCustomHeaders = await inquirer.prompt([
154
+ {
155
+ type: "confirm",
156
+ name: "add",
157
+ message: "Add custom headers?",
158
+ default: false,
159
+ },
160
+ ]);
161
+
162
+ if (wantsCustomHeaders.add) {
163
+ let addHeader = { continue: true };
164
+
165
+ while (addHeader.continue) {
166
+ addHeader = await inquirer.prompt([
167
+ {
168
+ type: "input",
169
+ name: "name",
170
+ message: "Custom header name:",
171
+ validate: (input) => !!input.length,
172
+ },
173
+ {
174
+ type: "input",
175
+ name: "value",
176
+ message: "Custom header value:",
177
+ validate: (input) => !!input.length,
178
+ },
179
+ {
180
+ type: "confirm",
181
+ name: "continue",
182
+ message: "Add another custom header?",
183
+ default: false,
184
+ },
185
+ ]);
186
+
187
+ if (addHeader.name && addHeader.value) {
188
+ headers.push({
189
+ name: addHeader.name,
190
+ value: addHeader.value,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+
41
198
  if (answers.stack === "fullstack") {
42
199
  answers = {
43
200
  ...answers,
@@ -135,20 +292,36 @@ module.exports = [
135
292
  setAWSClientDefaults(answers.client, answers.region);
136
293
 
137
294
  try {
138
- infastructure = await aws[answers.stack](
139
- slug,
140
- [
141
- { Key: "repository", Value: _.repo },
142
- { Key: "environment", Value: branch },
143
- { Key: "automated", Value: true },
144
- ],
145
- credentials,
146
- _.repoSafe,
147
- branch,
148
- answers.framework,
149
- answers.availability,
150
- answers.database,
151
- );
295
+ if (answers.stack === "static") {
296
+ infastructure = await aws.static(
297
+ slug,
298
+ [
299
+ { Key: "repository", Value: _.repo },
300
+ { Key: "environment", Value: branch },
301
+ { Key: "automated", Value: true },
302
+ ],
303
+ credentials,
304
+ _.repoSafe,
305
+ branch,
306
+ redirects,
307
+ headers,
308
+ );
309
+ } else {
310
+ infastructure = await aws.fullstack(
311
+ slug,
312
+ [
313
+ { Key: "repository", Value: _.repo },
314
+ { Key: "environment", Value: branch },
315
+ { Key: "automated", Value: true },
316
+ ],
317
+ credentials,
318
+ _.repoSafe,
319
+ branch,
320
+ answers.framework,
321
+ answers.availability,
322
+ answers.database,
323
+ );
324
+ }
152
325
  } catch (e) {
153
326
  console.log(e.message);
154
327
  process.exit(1);
@@ -175,6 +348,14 @@ module.exports = [
175
348
  config[branch].deploy.users = credentials;
176
349
  }
177
350
 
351
+ if (redirects.length) {
352
+ config[branch].deploy.redirects = redirects;
353
+ }
354
+
355
+ if (headers.length) {
356
+ config[branch].deploy.headers = headers;
357
+ }
358
+
178
359
  let stringify = JSON.stringify(config, null, 4);
179
360
  let output = stringify.substring(1, stringify.length - 1).trim();
180
361
  execSync(`printf '${output}' | pbcopy`);
@@ -0,0 +1,156 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ function handler(event) {
3
+ var query, index, key, response;
4
+
5
+ // Redirect if www to non-www
6
+ if (event.request.headers.host.value.includes("www.")) {
7
+ query = "";
8
+ index = 0;
9
+
10
+ for (key in event.request.querystring) {
11
+ query += `${index ? "&" : "?"}${key}=${event.request.querystring[key].value}`;
12
+ index++;
13
+ }
14
+
15
+ response = {
16
+ statusCode: 301,
17
+ statusDescription: "Moved Permanently",
18
+ headers: {
19
+ location: {
20
+ value: `https://${event.request.headers.host.value.split("www.")[1]}${event.request.uri}${query}`,
21
+ },
22
+ "strict-transport-security": {
23
+ value: "max-age=31536000; includeSubDomains",
24
+ },
25
+ },
26
+ };
27
+
28
+ return response;
29
+ }
30
+
31
+ // Check authentication if credentials are configured
32
+ var expected = "<%= credentials %>";
33
+
34
+ if (expected && expected.length > 0) {
35
+ var authHeaders = event.request.headers.authorization;
36
+
37
+ // If no auth header or credentials don't match, return 401
38
+ if (!authHeaders || !expected.find((d) => d === authHeaders.value)) {
39
+ response = {
40
+ statusCode: 401,
41
+ statusDescription: "Unauthorized",
42
+ headers: {
43
+ "www-authenticate": { value: "Basic" },
44
+ "strict-transport-security": {
45
+ value: "max-age=31536000; includeSubDomains",
46
+ },
47
+ "content-security-policy": {
48
+ value: "default-src 'self' https: data: 'unsafe-inline';",
49
+ },
50
+ "x-content-type-options": { value: "nosniff" },
51
+ "x-frame-options": { value: "sameorigin" },
52
+ },
53
+ };
54
+
55
+ return response;
56
+ }
57
+ }
58
+
59
+ // Redirect if no trailing slash (BEFORE custom redirects)
60
+ if (!event.request.uri.includes(".") && !event.request.uri.endsWith("/")) {
61
+ query = "";
62
+ index = 0;
63
+
64
+ for (key in event.request.querystring) {
65
+ query += `${index ? "&" : "?"}${key}=${event.request.querystring[key].value}`;
66
+ index++;
67
+ }
68
+
69
+ return {
70
+ statusCode: 301,
71
+ statusDescription: "Moved Permanently",
72
+ headers: { location: { value: `${event.request.uri}/${query}` } },
73
+ };
74
+ }
75
+
76
+ // Check for custom redirects (URI is guaranteed to have trailing slash now)
77
+ var customRedirects = "<%= redirects %>";
78
+ var currentUri = event.request.uri;
79
+ var i, redirect, srcPattern, captured, destination;
80
+
81
+ for (i = 0; i < customRedirects.length; i++) {
82
+ redirect = customRedirects[i];
83
+
84
+ // Handle wildcard redirects
85
+ if (redirect.src.endsWith("/*")) {
86
+ srcPattern = redirect.src.slice(0, -2);
87
+ if (
88
+ currentUri.startsWith(srcPattern + "/") ||
89
+ currentUri === srcPattern
90
+ ) {
91
+ captured = currentUri.slice(srcPattern.length);
92
+ destination = redirect.dest;
93
+
94
+ // Replace wildcard in destination with captured path
95
+ if (destination.endsWith("/*")) {
96
+ destination = destination.slice(0, -2) + captured;
97
+ } else {
98
+ destination = destination + captured;
99
+ }
100
+
101
+ // Handle relative vs absolute destinations
102
+ if (!destination.startsWith("http")) {
103
+ destination =
104
+ "https://" +
105
+ event.request.headers.host.value +
106
+ destination;
107
+ }
108
+
109
+ return {
110
+ statusCode: 301,
111
+ statusDescription: "Moved Permanently",
112
+ headers: { location: { value: destination } },
113
+ };
114
+ }
115
+ }
116
+ // Handle exact match redirects (normalize src to have trailing slash)
117
+ else {
118
+ var normalizedSrc = redirect.src.endsWith("/")
119
+ ? redirect.src
120
+ : redirect.src + "/";
121
+
122
+ if (normalizedSrc === currentUri) {
123
+ destination = redirect.dest;
124
+
125
+ // Handle relative vs absolute destinations
126
+ if (!destination.startsWith("http")) {
127
+ destination =
128
+ "https://" +
129
+ event.request.headers.host.value +
130
+ destination;
131
+ }
132
+
133
+ return {
134
+ statusCode: 301,
135
+ statusDescription: "Moved Permanently",
136
+ headers: { location: { value: destination } },
137
+ };
138
+ }
139
+ }
140
+ }
141
+
142
+ // Rewrite url to append index.html if not present
143
+ var request = event.request;
144
+ var uri = request.uri;
145
+
146
+ // Check whether the URI is missing a file name.
147
+ if (uri.endsWith("/")) {
148
+ request.uri += "index.html";
149
+ }
150
+ // Check whether the URI is missing a file extension.
151
+ else if (!uri.includes(".")) {
152
+ request.uri += "/index.html";
153
+ }
154
+
155
+ return request;
156
+ }
@@ -3,15 +3,26 @@ function handler(event) {
3
3
  // Add security headers
4
4
  var response = event.response;
5
5
  var headers = response.headers;
6
+ var customHeaders = "<%= headers %>";
7
+ var i, header;
6
8
 
7
- headers["strict-transport-security"] = {
8
- value: "max-age=31536000; includeSubDomains",
9
- };
10
- headers["content-security-policy"] = {
11
- value: "default-src 'self' https: data: 'unsafe-inline';",
12
- };
13
- headers["x-content-type-options"] = { value: "nosniff" };
14
- headers["x-frame-options"] = { value: "sameorigin" };
9
+ // If custom headers provided, use them; otherwise use defaults
10
+ if (customHeaders && customHeaders.length > 0) {
11
+ for (i = 0; i < customHeaders.length; i++) {
12
+ header = customHeaders[i];
13
+ headers[header.name] = { value: header.value };
14
+ }
15
+ } else {
16
+ // Default headers
17
+ headers["strict-transport-security"] = {
18
+ value: "max-age=31536000; includeSubDomains",
19
+ };
20
+ headers["content-security-policy"] = {
21
+ value: "default-src 'self' https: data: 'unsafe-inline';",
22
+ };
23
+ headers["x-content-type-options"] = { value: "nosniff" };
24
+ headers["x-frame-options"] = { value: "sameorigin" };
25
+ }
15
26
 
16
27
  return response;
17
28
  }
@@ -66,6 +66,8 @@ module.exports.static = async (
66
66
  credentials = [],
67
67
  repo,
68
68
  branch,
69
+ redirects = [],
70
+ headers = [],
69
71
  ) => {
70
72
  let s3 = await module.exports.s3.createS3Bucket(name, tags);
71
73
 
@@ -76,14 +78,13 @@ module.exports.static = async (
76
78
  (
77
79
  await module.exports.cloudfront.createCloudFrontFunction(
78
80
  name,
79
- credentials.length
80
- ? "aws-cloudfront-auth"
81
- : "aws-cloudfront-simple",
81
+ "aws-cloudfront-request",
82
82
  {
83
83
  credentials: credentials.map(
84
84
  (d) =>
85
85
  `Basic ${Buffer.from(`${d.username}:${d.password}`).toString("base64")}`,
86
86
  ),
87
+ redirects: redirects,
87
88
  },
88
89
  )
89
90
  ).FunctionSummary.FunctionMetadata.FunctionARN,
@@ -95,7 +96,9 @@ module.exports.static = async (
95
96
  branch,
96
97
  ),
97
98
  "aws-cloudfront-response",
98
- {},
99
+ {
100
+ headers: headers,
101
+ },
99
102
  )
100
103
  ).FunctionSummary.FunctionMetadata.FunctionARN,
101
104
  );
@@ -143,7 +146,11 @@ module.exports.staticTerminate = async (name, repo, branch, id) => {
143
146
 
144
147
  try {
145
148
  await module.exports.cloudfront.removeCloudFrontFunction(
146
- module.exports.slug(`${repo}-response`, branch),
149
+ module.exports.slug(
150
+ `${repo}-response`,
151
+ process.env.AWS_PROFILE,
152
+ branch,
153
+ ),
147
154
  );
148
155
  } catch {
149
156
  /* empty */
package/globals.js CHANGED
@@ -847,7 +847,7 @@ module.exports = {
847
847
  let alreadyRunning = running;
848
848
 
849
849
  if (!running) {
850
- execSync(`lab-env up -d`, opts);
850
+ execSync(`${docker} up -d`, opts);
851
851
  running = true;
852
852
  }
853
853
 
@@ -858,7 +858,7 @@ module.exports = {
858
858
  throw e;
859
859
  } finally {
860
860
  if (!alreadyRunning) {
861
- execSync(`lab-env down`, opts);
861
+ execSync(`${docker} down`, opts);
862
862
  running = false;
863
863
  }
864
864
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "5.2.0",
3
+ "version": "5.3.0-beta.1",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -2,7 +2,7 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm AS development
2
2
 
3
3
  LABEL org.opencontainers.image.authors="Mike Mellor <mike.mellor@avalerehealth.com>"
4
4
 
5
- FROM fishawack/lab-env-laravel-8-nginx:latest AS production
5
+ FROM fishawack/lab-env-python-0:latest AS production
6
6
 
7
7
  # Copy source code into container
8
8
  COPY . /app
@@ -1,93 +0,0 @@
1
- // eslint-disable-next-line no-unused-vars
2
- function handler(event) {
3
- var query, index, key, response;
4
-
5
- // Redirect if www to non-www
6
- if (event.request.headers.host.value.includes("www.")) {
7
- query = "";
8
- index = 0;
9
-
10
- for (key in event.request.querystring) {
11
- query += `${index ? "&" : "?"}${key}=${event.request.querystring[key].value}`;
12
- index++;
13
- }
14
-
15
- response = {
16
- statusCode: 301,
17
- statusDescription: "Moved Permanently",
18
- headers: {
19
- location: {
20
- value: `https://${event.request.headers.host.value.split("www.")[1]}${event.request.uri}${query}`,
21
- },
22
- "strict-transport-security": {
23
- value: "max-age=31536000; includeSubDomains",
24
- },
25
- },
26
- };
27
-
28
- return response;
29
- }
30
-
31
- // Redirect if no trailing slash
32
- if (!event.request.uri.includes(".") && !event.request.uri.endsWith("/")) {
33
- query = "";
34
- index = 0;
35
-
36
- for (key in event.request.querystring) {
37
- query += `${index ? "&" : "?"}${key}=${event.request.querystring[key].value}`;
38
- index++;
39
- }
40
-
41
- return {
42
- statusCode: 301,
43
- statusDescription: "Moved Permanently",
44
- headers: { location: { value: `${event.request.uri}/${query}` } },
45
- };
46
- }
47
-
48
- var authHeaders = event.request.headers.authorization;
49
-
50
- // The Base64-encoded Auth string that should be present.
51
- // It is an encoding of `Basic base64([username]:[password])`
52
- var expected = "<%= credentials %>";
53
-
54
- // If an Authorization header is supplied and it's an exact match, pass the
55
- // request on through to CF/the origin without any modification.
56
- if (authHeaders && expected.find((d) => d === authHeaders.value)) {
57
- // Rewrite url to append index.html if not present
58
- var request = event.request;
59
- var uri = request.uri;
60
-
61
- // Check whether the URI is missing a file name.
62
- if (uri.endsWith("/")) {
63
- request.uri += "index.html";
64
- }
65
- // Check whether the URI is missing a file extension.
66
- else if (!uri.includes(".")) {
67
- request.uri += "/index.html";
68
- }
69
-
70
- return request;
71
- }
72
-
73
- // But if we get here, we must either be missing the auth header or the
74
- // credentials failed to match what we expected.
75
- // Request the browser present the Basic Auth dialog.
76
- response = {
77
- statusCode: 401,
78
- statusDescription: "Unauthorized",
79
- headers: {
80
- "www-authenticate": { value: "Basic" },
81
- "strict-transport-security": {
82
- value: "max-age=31536000; includeSubDomains",
83
- },
84
- "content-security-policy": {
85
- value: "default-src 'self' https: data: 'unsafe-inline';",
86
- },
87
- "x-content-type-options": { value: "nosniff" },
88
- "x-frame-options": { value: "sameorigin" },
89
- },
90
- };
91
-
92
- return response;
93
- }
@@ -1,61 +0,0 @@
1
- // eslint-disable-next-line no-unused-vars
2
- function handler(event) {
3
- var query, index, key, response;
4
-
5
- // Redirect if www to non-www
6
- if (event.request.headers.host.value.includes("www.")) {
7
- query = "";
8
- index = 0;
9
-
10
- for (key in event.request.querystring) {
11
- query += `${index ? "&" : "?"}${key}=${event.request.querystring[key].value}`;
12
- index++;
13
- }
14
-
15
- response = {
16
- statusCode: 301,
17
- statusDescription: "Moved Permanently",
18
- headers: {
19
- location: {
20
- value: `https://${event.request.headers.host.value.split("www.")[1]}${event.request.uri}${query}`,
21
- },
22
- "strict-transport-security": {
23
- value: "max-age=31536000; includeSubDomains",
24
- },
25
- },
26
- };
27
-
28
- return response;
29
- }
30
-
31
- // Redirect if no trailing slash
32
- if (!event.request.uri.includes(".") && !event.request.uri.endsWith("/")) {
33
- query = "";
34
- index = 0;
35
-
36
- for (key in event.request.querystring) {
37
- query += `${index ? "&" : "?"}${key}=${event.request.querystring[key].value}`;
38
- index++;
39
- }
40
-
41
- return {
42
- statusCode: 301,
43
- statusDescription: "Moved Permanently",
44
- headers: { location: { value: `${event.request.uri}/${query}` } },
45
- };
46
- }
47
-
48
- var request = event.request;
49
- var uri = request.uri;
50
-
51
- // Check whether the URI is missing a file name.
52
- if (uri.endsWith("/")) {
53
- request.uri += "index.html";
54
- }
55
- // Check whether the URI is missing a file extension.
56
- else if (!uri.includes(".")) {
57
- request.uri += "/index.html";
58
- }
59
-
60
- return request;
61
- }