FIx: preserve Cookie header and cache Omada session

This commit is contained in:
shamoon
2026-04-10 09:37:19 -07:00
parent d048888d99
commit bb615e759a
4 changed files with 188 additions and 30 deletions

View File

@@ -7,7 +7,10 @@ export function setCookieHeader(url, params) {
const existingCookie = cookieJar.getCookieStringSync(url.toString());
if (existingCookie) {
params.headers = params.headers ?? {};
params.headers[params.cookieHeader ?? "Cookie"] = existingCookie;
const cookieHeader = params.cookieHeader ?? "Cookie";
if (!params.headers[cookieHeader]) {
params.headers[cookieHeader] = existingCookie;
}
}
}

View File

@@ -42,4 +42,16 @@ describe("utils/proxy/cookie-jar", () => {
expect(params.headers.Cookie).toContain("c=d");
});
it("does not overwrite an explicit cookie header", async () => {
const { addCookieToJar, setCookieHeader } = await import("./cookie-jar");
const url = new URL("http://example4.test/path");
addCookieToJar(url, { "set-cookie": ["sid=1; Path=/"] });
const params = { headers: { Cookie: "manual=1" } };
setCookieHeader(url, params);
expect(params.headers.Cookie).toBe("manual=1");
});
});

View File

@@ -1,12 +1,30 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { httpProxy } from "utils/proxy/http";
const proxyName = "omadaProxyHandler";
const sessionCacheKey = `${proxyName}__session`;
const logger = createLogger(proxyName);
async function login(loginUrl, username, password, controllerVersionMajor) {
function getSessionCacheId(service) {
return `${sessionCacheKey}.${service}`;
}
function getCookieHeader(responseHeaders) {
const setCookie = responseHeaders?.["set-cookie"];
if (!setCookie) return null;
const cookies = (Array.isArray(setCookie) ? setCookie : [setCookie])
.map((cookie) => cookie.split(";")[0])
.filter(Boolean);
return cookies.length > 0 ? cookies.join("; ") : null;
}
async function login(loginUrl, username, password, controllerVersionMajor, service) {
const params = {
username,
password,
@@ -20,7 +38,7 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
};
}
const [status, contentType, data] = await httpProxy(loginUrl, {
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify(params),
headers: {
@@ -28,7 +46,20 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
},
});
return [status, JSON.parse(data.toString())];
const loginResponseData = JSON.parse(data.toString());
if (status === 200 && loginResponseData.errorCode === 0) {
cache.put(
getSessionCacheId(service),
{
token: loginResponseData.result.token,
cookieHeader: getCookieHeader(responseHeaders),
},
55 * 60 * 1000, // Cache session for 55 minutes
);
}
return [status, loginResponseData];
}
export default async function omadaProxyHandler(req, res) {
@@ -86,25 +117,34 @@ export default async function omadaProxyHandler(req, res) {
break;
}
const [loginStatus, loginResponseData] = await login(
loginUrl,
widget.username,
widget.password,
controllerVersionMajor,
);
let session = cache.get(getSessionCacheId(service));
if (!session) {
const [loginStatus, loginResponseData] = await login(
loginUrl,
widget.username,
widget.password,
controllerVersionMajor,
service,
);
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
return res
.status(loginStatus)
.json({ error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData } });
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
return res.status(loginStatus).json({
error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData },
});
}
session = cache.get(getSessionCacheId(service));
}
const { token } = loginResponseData.result;
const { token, cookieHeader } = session;
let sitesUrl;
let body = {};
let params = { token };
let headers = { "Csrf-Token": token };
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
let method = "GET";
switch (controllerVersionMajor) {
@@ -116,6 +156,10 @@ export default async function omadaProxyHandler(req, res) {
userName: widget.username,
},
};
headers = { "Content-Type": "application/json" };
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
method = "POST";
break;
case 4:
@@ -174,6 +218,9 @@ export default async function omadaProxyHandler(req, res) {
},
};
headers = { "Content-Type": "application/json" };
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
params = { token };
[status, contentType, data] = await httpProxy(switchUrl, {
@@ -216,9 +263,7 @@ export default async function omadaProxyHandler(req, res) {
: `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(siteStatsUrl, {
headers: {
"Csrf-Token": token,
},
headers,
});
siteResponseData = JSON.parse(data);
@@ -240,9 +285,7 @@ export default async function omadaProxyHandler(req, res) {
: `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(alertUrl, {
headers: {
"Csrf-Token": token,
},
headers,
});
const alertResponseData = JSON.parse(data);

View File

@@ -2,14 +2,24 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
logger: {
debug: vi.fn(),
error: vi.fn(),
},
}));
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => store.get(k)),
put: vi.fn((k, v) => store.set(k, v)),
del: vi.fn((k) => store.delete(k)),
_reset: () => store.clear(),
},
logger: {
debug: vi.fn(),
error: vi.fn(),
},
};
});
vi.mock("utils/logger", () => ({
default: () => logger,
@@ -20,15 +30,19 @@ vi.mock("utils/config/service-helpers", () => ({
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
import omadaProxyHandler from "./proxy";
describe("widgets/omada/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
// Clear one-off implementations between tests (some branches return early).
httpProxy.mockReset();
getServiceWidget.mockReset();
cache._reset();
});
it("fetches controller info, logs in, selects site, and returns overview stats (v4)", async () => {
@@ -51,6 +65,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
// sites list
.mockResolvedValueOnce([
@@ -91,6 +106,12 @@ describe("widgets/omada/proxy", () => {
connectedSwitches: 3,
}),
);
expect(httpProxy.mock.calls[2][1]).toMatchObject({
headers: {
"Csrf-Token": "t",
Cookie: "TPOMADA_SESSIONID=sid",
},
});
});
it("returns an error when controller info cannot be retrieved", async () => {
@@ -169,6 +190,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 2, msg: "bad" })]);
@@ -195,6 +217,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
@@ -222,6 +245,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
// getUserSites
.mockResolvedValueOnce([
@@ -271,6 +295,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
@@ -301,6 +326,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
@@ -324,4 +350,78 @@ describe("widgets/omada/proxy", () => {
},
});
});
it("reuses a cached Omada session across polls", async () => {
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
const req = { query: { group: "g", service: "svc", index: "0" } };
await omadaProxyHandler(req, createMockRes());
await omadaProxyHandler(req, createMockRes());
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
expect(loginCalls).toHaveLength(1);
expect(httpProxy.mock.calls[6][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid");
});
});