mirror of
https://github.com/gethomepage/homepage.git
synced 2026-04-10 20:21:20 -07:00
FIx: preserve Cookie header and cache Omada session
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}¤tPage=1¤tPageSize=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}¤tPage=1¤tPageSize=1000`;
|
||||
|
||||
[status, contentType, data] = await httpProxy(alertUrl, {
|
||||
headers: {
|
||||
"Csrf-Token": token,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
const alertResponseData = JSON.parse(data);
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user