@antelopejs/dms-frontend 0.5.4 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -0,0 +1,53 @@
1
+ import { writeBadRequest } from "./request-failure.mjs";
2
+
3
+ // The request target as the server reads it. Node hands `request.url` over
4
+ // verbatim, so it is not always a path: `//host/x`, `/\host/x` and the
5
+ // absolute form `http://host/x` all name another host once resolved, which
6
+ // turned the proxy into an open one. Every routing decision and the URL sent to
7
+ // the backend read the same normalized path from here.
8
+
9
+ const FRONTEND_ORIGIN = "http://frontend.local";
10
+
11
+ /**
12
+ * Whether `target` is an origin-form request target: a path, optionally with a
13
+ * query, that stays on this server once resolved. Anything else (an absolute
14
+ * or authority form, `*`, a path a URL parser reads as another host) is not a
15
+ * request a browser sends to the frontend.
16
+ */
17
+ export function isOriginForm(target) {
18
+ if (typeof target !== "string" || !target.startsWith("/")) return false;
19
+ try {
20
+ return new URL(target, FRONTEND_ORIGIN).origin === FRONTEND_ORIGIN;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * The backend URL a request target is proxied to: the target's normalized path
28
+ * and query on the backend's origin. The path is set, not resolved, so a path
29
+ * normalizing to `//host` (`/x/..//host`) stays a path on the backend.
30
+ *
31
+ * @param target The request target, origin-form
32
+ * @param backendBaseUrl The backend's base URL
33
+ */
34
+ export function backendUrl(
35
+ target,
36
+ backendBaseUrl = process.env.DMS_API_BASE_URL,
37
+ ) {
38
+ const { pathname, search } = new URL(target, FRONTEND_ORIGIN);
39
+ const url = new URL(new URL(backendBaseUrl).origin);
40
+ url.pathname = pathname;
41
+ url.search = search;
42
+ return url;
43
+ }
44
+
45
+ /**
46
+ * Answers 400 to a request whose target is not origin-form, before any route
47
+ * reads it. Returns whether it answered.
48
+ */
49
+ export function refuseForeignTarget(request, response) {
50
+ if (isOriginForm(request.url)) return false;
51
+ writeBadRequest(response);
52
+ return true;
53
+ }
@@ -42,6 +42,7 @@ import { HOMEPAGE } from "./server/homepage.mjs";
42
42
  import { htmlTag } from "./server/html-tag.mjs";
43
43
  import * as icons from "./server/icons.mjs";
44
44
  import * as requestFailure from "./server/request-failure.mjs";
45
+ import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
45
46
  import { handleTester } from "./server/tester.mjs";
46
47
  import {
47
48
  createInertiaPage,
@@ -239,12 +240,11 @@ async function proxy(request, response, fallbackOnNotFound = false) {
239
240
  refuseCrossOrigin(response);
240
241
  return true;
241
242
  }
242
- const target = new URL(request.url, process.env.DMS_API_BASE_URL);
243
243
  // Cancelling the body alone misses a client that leaves before the backend
244
244
  // answers: its stream, an SSE feed above all, then stayed open for good.
245
245
  const abandoned = new AbortController();
246
246
  response.once("close", () => abandoned.abort());
247
- const upstream = await fetch(target, {
247
+ const upstream = await fetch(backendUrl(request.url), {
248
248
  signal: abandoned.signal,
249
249
  method: request.method,
250
250
  headers: backendHeaders(request, publicEndpoint),
@@ -505,6 +505,7 @@ async function writeRequestFailure(error, request, response) {
505
505
 
506
506
  export async function handleRequestSafely(request, response) {
507
507
  try {
508
+ if (refuseForeignTarget(request, response)) return;
508
509
  await handleRequest(request, response);
509
510
  } catch (error) {
510
511
  // An error page failing too must not reject: that would end the process.