mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(egress): keep loopback grantable under an exact operator origin
A Nightscout self-host on host networking with NIGHTSCOUT_PRIVATE_ORIGINS=http://localhost:1337 worked before and must keep working unchanged; an operator who lists an exact loopback origin with its port has made the same deliberate decision as one who lists a LAN origin. The never-grantable floor is now the unspecified address, link-local and the metadata endpoint only. Loopback (127/8, ::1, localhost) is grantable when listed exactly, still resolved and pinned at connect time with redirects forbidden, and still refused when not listed. ::1 is answered before the embedded-IPv4 unwrap, which read it as the IPv4-compatible spelling of 0.0.0.1 and refused it as unspecified. The pinned Nightscout transport test runs the real floor again. Each refused range keeps its own test; loopback moves to the grantable-when-listed set with a test that an unlisted loopback is still refused, and a Nightscout grant of http://localhost:1337 is asserted to parse. Refs #947
This commit is contained in:
1 parent
9c306f04e7
commit
94bed12edc
31 files changed
+236
-156
No files matched your search
@@ -179,25 +179,47 @@ describe("PUT /api/settings/webhook — private targets", () => {
|
||||
expect(json.meta).toBeUndefined();
|
||||
});
|
||||
|
||||
it("answers a loopback target with the not-grantable code, listed or not (M1)", async () => {
|
||||
it("saves a listed loopback target and refuses an unlisted one with the listable code", async () => {
|
||||
// Host networking: an exact loopback origin with its port is a grant like
|
||||
// any other. Unlisted, it is refused with the code that says "list it",
|
||||
// because listing it does open it.
|
||||
process.env.NOTIFICATION_PRIVATE_ORIGINS = "http://localhost:8080";
|
||||
|
||||
const refused = await put("webhook", {
|
||||
url: "http://127.0.0.1:8080/message",
|
||||
enabled: true,
|
||||
});
|
||||
expect(refused.status).toBe(422);
|
||||
await expect(refused.json()).resolves.toMatchObject({
|
||||
meta: { errorCode: "private_origin_not_approved" },
|
||||
});
|
||||
|
||||
const saved = await put("webhook", {
|
||||
url: "http://localhost:8080/message",
|
||||
enabled: true,
|
||||
});
|
||||
expect(saved.status).toBe(200);
|
||||
expect(prisma.notificationChannel.upsert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("answers a metadata or unspecified target with the not-grantable code, listed or not (M1)", async () => {
|
||||
// This used to pin a dead end: the route said "list it", the parser
|
||||
// dropped the listing, and the next save said "list it" again. A target
|
||||
// no grant can open gets its own code and a message that says so.
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
process.env.NOTIFICATION_PRIVATE_ORIGINS =
|
||||
"http://127.0.0.1:8080, http://localhost:8080";
|
||||
"http://169.254.169.254, http://0.0.0.0:8080";
|
||||
|
||||
for (const url of [
|
||||
"http://127.0.0.1:8080/message",
|
||||
"http://localhost:8080/message",
|
||||
"http://169.254.169.254/latest",
|
||||
"http://[::1]:8080/message",
|
||||
"http://0.0.0.0:8080/message",
|
||||
"http://[fe80::1]:8080/message",
|
||||
]) {
|
||||
const response = await put("webhook", { url, enabled: true });
|
||||
expect(response.status).toBe(422);
|
||||
const json = await response.json();
|
||||
expect(json.meta).toEqual({ errorCode: "private_origin_not_grantable" });
|
||||
expect(json.error).toMatch(/LAN address/);
|
||||
expect(json.error).toMatch(/no operator grant can open/);
|
||||
expect(json.error).not.toContain("NOTIFICATION_PRIVATE_ORIGINS");
|
||||
expect(json.details.issues.length).toBeGreaterThan(0);
|
||||
}
|
||||
@@ -321,7 +343,7 @@ describe("POST /api/settings/{webhook,ntfy}/test — the refusal names itself",
|
||||
expect(response.status).toBe(422);
|
||||
const json = await response.json();
|
||||
expect(json.meta).toEqual({ errorCode: "private_origin_not_grantable" });
|
||||
expect(json.error).toMatch(/LAN address/);
|
||||
expect(json.error).toMatch(/no operator grant can open/);
|
||||
expect(json.error).not.toContain("NOTIFICATION_PRIVATE_ORIGINS");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ const ntfySettingsSchema = ntfySettingsSchemaWith(isAllowedNotificationTarget);
|
||||
const PRIVATE_ORIGIN_REFUSAL =
|
||||
"This server is on a private network. The operator has to list its exact origin (scheme://host:port) in NOTIFICATION_PRIVATE_ORIGINS before HealthLog can send to it.";
|
||||
const NOT_GRANTABLE_REFUSAL =
|
||||
"This server is loopback, link-local or a metadata address, which no operator grant can open. Use the relay's LAN address instead.";
|
||||
"This server is a link-local, metadata or unspecified address, which no operator grant can open. Use the address the relay actually listens on.";
|
||||
|
||||
/**
|
||||
* Refusal for an enable attempt on a channel the operator switched off
|
||||
|
||||
@@ -52,7 +52,7 @@ export const POST = apiHandler(async () => {
|
||||
});
|
||||
return apiError(
|
||||
result.errorCode === "private_origin_not_grantable"
|
||||
? "The server is loopback, link-local or a metadata address, which no operator grant can open. Use the relay's LAN address instead."
|
||||
? "The server is a link-local, metadata or unspecified address, which no operator grant can open. Use the address the relay actually listens on."
|
||||
: "The server is on a private network the operator has not approved. List its exact origin (scheme://host:port) in NOTIFICATION_PRIVATE_ORIGINS on the server.",
|
||||
422,
|
||||
{ errorCode: result.errorCode },
|
||||
|
||||
@@ -34,7 +34,7 @@ const webhookSettingsSchema = webhookSettingsSchemaWith(
|
||||
const PRIVATE_ORIGIN_REFUSAL =
|
||||
"This address is on a private network. The operator has to list its exact origin (scheme://host:port) in NOTIFICATION_PRIVATE_ORIGINS before HealthLog can send to it.";
|
||||
const NOT_GRANTABLE_REFUSAL =
|
||||
"This address is loopback, link-local or a metadata address, which no operator grant can open. Use the relay's LAN address instead.";
|
||||
"This address is a link-local, metadata or unspecified address, which no operator grant can open. Use the address the relay actually listens on.";
|
||||
|
||||
/**
|
||||
* Generic-webhook channel config (v1.17.1).
|
||||
|
||||
@@ -51,7 +51,7 @@ export const POST = apiHandler(async () => {
|
||||
});
|
||||
return apiError(
|
||||
result.errorCode === "private_origin_not_grantable"
|
||||
? "The target is loopback, link-local or a metadata address, which no operator grant can open. Use the relay's LAN address instead."
|
||||
? "The target is a link-local, metadata or unspecified address, which no operator grant can open. Use the address the relay actually listens on."
|
||||
: "The target is on a private network the operator has not approved. List its exact origin (scheme://host:port) in NOTIFICATION_PRIVATE_ORIGINS on the server.",
|
||||
422,
|
||||
{ errorCode: result.errorCode },
|
||||
|
||||
Reference in new issue
Block a user