Compare commits

...
Author SHA1 Message Date
Stefan AgnerandClaude Fable 5.1 d320a50233 Use dedicated websocket commands for app ingress sessions
The ingress panel used the generic supervisor/api websocket command to read
app info, create an ingress session and keep it alive. Core exposed those
three endpoints to non-admin users for apps with panel_admin: false, but
could not check which app was requested, so any user could open any app's
ingress UI.

Core now provides supervisor/ingress/info, supervisor/ingress/session and
supervisor/ingress/validate_session, which take the app slug and authorize
the request per app. Switch the ingress panel to these commands and use a
slim IngressAppInfo type with just the fields the panel needs. The generic
supervisor/api command becomes admin-only in Core.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-08 10:18:23 +02:00
3 changed files with 44 additions and 29 deletions
+20 -10
View File
@@ -1,5 +1,6 @@
import { getCollection, type Connection } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
import type { AddonState } from "./addon";
import { supervisorApiWsRequest } from "../supervisor/supervisor";
function setIngressCookie(session: string): string {
@@ -9,13 +10,27 @@ function setIngressCookie(session: string): string {
return session;
}
export interface IngressAppInfo {
name: string;
slug: string;
version: string | null;
state: AddonState | null;
ingress_url: string | null;
}
export const fetchIngressAppInfo = (
hass: Pick<HomeAssistant, "callWS">,
slug: string
): Promise<IngressAppInfo> =>
hass.callWS({ type: "supervisor/ingress/info", slug });
export const createHassioSession = async (
hass: Pick<HomeAssistant, "callWS">
hass: Pick<HomeAssistant, "callWS">,
slug: string
): Promise<string> => {
const wsResponse: { session: string } = await hass.callWS({
type: "supervisor/api",
endpoint: "/ingress/session",
method: "post",
type: "supervisor/ingress/session",
slug,
});
return setIngressCookie(wsResponse.session);
};
@@ -43,10 +58,5 @@ export const validateHassioSession = async (
hass: Pick<HomeAssistant, "callWS">,
session: string
): Promise<void> => {
await hass.callWS({
type: "supervisor/api",
endpoint: "/ingress/validate_session",
method: "post",
data: { session },
});
await hass.callWS({ type: "supervisor/ingress/validate_session", session });
};
+8 -10
View File
@@ -10,14 +10,12 @@ import { navigate } from "../../common/navigate";
import { computeRouteTail } from "../../common/url/route";
import { nextRender } from "../../common/util/render-status";
import "../../components/ha-icon-button";
import type { HassioAddonDetails } from "../../data/hassio/addon";
import {
fetchHassioAddonInfo,
startHassioAddon,
} from "../../data/hassio/addon";
import { startHassioAddon } from "../../data/hassio/addon";
import { extractApiErrorMessage } from "../../data/hassio/common";
import type { IngressAppInfo } from "../../data/hassio/ingress";
import {
createHassioSession,
fetchIngressAppInfo,
validateHassioSession,
} from "../../data/hassio/ingress";
import {
@@ -45,7 +43,7 @@ class HaPanelApp extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _addon?: HassioAddonDetails;
@state() private _addon?: IngressAppInfo;
@state() private _loadingMessage?: string;
@@ -227,12 +225,12 @@ class HaPanelApp extends LitElement {
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass);
const createSessionPromise = createHassioSession(this.hass, addonSlug);
let addon: HassioAddonDetails;
let addon: IngressAppInfo;
try {
addon = await fetchHassioAddonInfo(this.hass.callWS, addonSlug);
addon = await fetchIngressAppInfo(this.hass, addonSlug);
} catch (err: any) {
await this._showErrorAndNavigateHome(
addonSlug,
@@ -335,7 +333,7 @@ class HaPanelApp extends LitElement {
try {
await validateHassioSession(this.hass, session);
} catch (_err: any) {
session = await createHassioSession(this.hass);
session = await createHassioSession(this.hass, addonSlug);
}
}, 60000);
+16 -9
View File
@@ -4,9 +4,13 @@ import type { HomeAssistant } from "../../src/types";
describe("Create hassio session", () => {
const hass = {
callWS: async () => ({
session: "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh",
}),
callWS: async (msg: Record<string, unknown>) => {
assert.deepStrictEqual(msg, {
type: "supervisor/ingress/session",
slug: "core_test",
});
return { session: "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh" };
},
} as unknown as Pick<HomeAssistant, "callWS">;
afterEach(() => {
@@ -16,7 +20,7 @@ describe("Create hassio session", () => {
it("Test create session without HTTPS", async () => {
vi.stubGlobal("document", {});
vi.stubGlobal("location", { protocol: "http:" });
await createHassioSession(hass);
await createHassioSession(hass, "core_test");
assert.strictEqual(
global.document.cookie,
"ingress_session=fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh;path=/api/hassio_ingress/;SameSite=Strict"
@@ -25,18 +29,21 @@ describe("Create hassio session", () => {
it("Test create session with HTTPS", async () => {
vi.stubGlobal("document", {});
vi.stubGlobal("location", { protocol: "https:" });
await createHassioSession(hass);
await createHassioSession(hass, "core_test");
assert.strictEqual(
global.document.cookie,
"ingress_session=fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh;path=/api/hassio_ingress/;SameSite=Strict;Secure"
);
});
it("Test fail to create", async () => {
const createSessionPromise = createHassioSession({
callWS: async () => {
throw new Error("Failed to create session");
const createSessionPromise = createHassioSession(
{
callWS: async () => {
throw new Error("Failed to create session");
},
},
}).then(
"core_test"
).then(
() => true,
() => false
);