mirror of
https://github.com/gethomepage/homepage.git
synced 2026-03-30 23:02:39 -07:00
Compare commits
2 Commits
v1.11.0
...
feature/om
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48d283dc2 | ||
|
|
d313e0a124 |
@@ -6,6 +6,40 @@ const proxyName = "omadaProxyHandler";
|
||||
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
function parseOmadaJson(data, { step, status, contentType, url }) {
|
||||
const body = Buffer.isBuffer(data) ? data.toString() : String(data ?? "");
|
||||
|
||||
try {
|
||||
return JSON.parse(body);
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
"Failed parsing Omada %s response as JSON (HTTP %d, content-type: %s, url: %s). Body: %s",
|
||||
step,
|
||||
status,
|
||||
contentType ?? "unknown",
|
||||
url,
|
||||
body,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function isLikelyHtmlResponse(contentType, data) {
|
||||
const body = Buffer.isBuffer(data) ? data.toString() : String(data ?? "");
|
||||
return contentType?.includes("text/html") || body.startsWith("<!DOCTYPE") || body.startsWith("<html");
|
||||
}
|
||||
|
||||
function extractCookieHeader(responseHeaders) {
|
||||
const setCookieHeader = responseHeaders?.["set-cookie"];
|
||||
if (!setCookieHeader) return undefined;
|
||||
|
||||
if (Array.isArray(setCookieHeader)) {
|
||||
return setCookieHeader.map((cookie) => cookie.split(";")[0]).join("; ");
|
||||
}
|
||||
|
||||
return String(setCookieHeader).split(";")[0];
|
||||
}
|
||||
|
||||
async function login(loginUrl, username, password, controllerVersionMajor) {
|
||||
const params = {
|
||||
username,
|
||||
@@ -20,15 +54,17 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
|
||||
};
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(loginUrl, {
|
||||
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
|
||||
method: "POST",
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
return [status, JSON.parse(data.toString())];
|
||||
return [status, contentType, data, extractCookieHeader(responseHeaders)];
|
||||
}
|
||||
|
||||
export default async function omadaProxyHandler(req, res) {
|
||||
@@ -86,12 +122,18 @@ export default async function omadaProxyHandler(req, res) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [loginStatus, loginResponseData] = await login(
|
||||
const [loginStatus, loginContentType, loginData, loginCookieHeader] = await login(
|
||||
loginUrl,
|
||||
widget.username,
|
||||
widget.password,
|
||||
controllerVersionMajor,
|
||||
);
|
||||
const loginResponseData = parseOmadaJson(loginData, {
|
||||
step: "login",
|
||||
status: loginStatus,
|
||||
contentType: loginContentType,
|
||||
url: loginUrl,
|
||||
});
|
||||
|
||||
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
|
||||
return res
|
||||
@@ -100,11 +142,13 @@ export default async function omadaProxyHandler(req, res) {
|
||||
}
|
||||
|
||||
const { token } = loginResponseData.result;
|
||||
let omadaCookieHeader = loginCookieHeader;
|
||||
|
||||
let sitesUrl;
|
||||
let body = {};
|
||||
let params = { token };
|
||||
let headers = { "Csrf-Token": token };
|
||||
if (omadaCookieHeader) headers.Cookie = omadaCookieHeader;
|
||||
let method = "GET";
|
||||
|
||||
switch (controllerVersionMajor) {
|
||||
@@ -134,9 +178,72 @@ export default async function omadaProxyHandler(req, res) {
|
||||
params,
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
const sitesResponseData = JSON.parse(data);
|
||||
let sitesResponseData;
|
||||
try {
|
||||
sitesResponseData = parseOmadaJson(data, {
|
||||
step: "sites list",
|
||||
status,
|
||||
contentType,
|
||||
url: sitesUrl,
|
||||
});
|
||||
} catch (parseError) {
|
||||
if (!isLikelyHtmlResponse(contentType, data)) {
|
||||
throw parseError;
|
||||
}
|
||||
|
||||
logger.debug("Received HTML response for Omada sites list; retrying with a fresh login.");
|
||||
|
||||
const [retryLoginStatus, retryLoginContentType, retryLoginData, retryLoginCookieHeader] = await login(
|
||||
loginUrl,
|
||||
widget.username,
|
||||
widget.password,
|
||||
controllerVersionMajor,
|
||||
);
|
||||
const retryLoginResponseData = parseOmadaJson(retryLoginData, {
|
||||
step: "login (retry)",
|
||||
status: retryLoginStatus,
|
||||
contentType: retryLoginContentType,
|
||||
url: loginUrl,
|
||||
});
|
||||
|
||||
if (retryLoginStatus !== 200 || retryLoginResponseData.errorCode > 0) {
|
||||
return res.status(retryLoginStatus).json({
|
||||
error: {
|
||||
message: "Error re-authenticating to Omada controller",
|
||||
url: loginUrl,
|
||||
data: retryLoginResponseData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const retryToken = retryLoginResponseData.result?.token;
|
||||
omadaCookieHeader = retryLoginCookieHeader;
|
||||
const retrySitesUrlObj = new URL(sitesUrl);
|
||||
retrySitesUrlObj.searchParams.set("token", retryToken);
|
||||
const retrySitesUrl = retrySitesUrlObj.toString();
|
||||
|
||||
[status, contentType, data] = await httpProxy(retrySitesUrl, {
|
||||
method,
|
||||
params: { token: retryToken },
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
...headers,
|
||||
"Csrf-Token": retryToken,
|
||||
...(omadaCookieHeader ? { Cookie: omadaCookieHeader } : {}),
|
||||
},
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
sitesResponseData = parseOmadaJson(data, {
|
||||
step: "sites list (retry)",
|
||||
status,
|
||||
contentType,
|
||||
url: retrySitesUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||
@@ -174,6 +281,7 @@ export default async function omadaProxyHandler(req, res) {
|
||||
},
|
||||
};
|
||||
headers = { "Content-Type": "application/json" };
|
||||
if (omadaCookieHeader) headers.Cookie = omadaCookieHeader;
|
||||
params = { token };
|
||||
|
||||
[status, contentType, data] = await httpProxy(switchUrl, {
|
||||
@@ -181,9 +289,15 @@ export default async function omadaProxyHandler(req, res) {
|
||||
params,
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
const switchResponseData = JSON.parse(data);
|
||||
const switchResponseData = parseOmadaJson(data, {
|
||||
step: "switch site",
|
||||
status,
|
||||
contentType,
|
||||
url: switchUrl,
|
||||
});
|
||||
if (status !== 200 || switchResponseData.errorCode > 0) {
|
||||
logger.error(`HTTP ${status} getting sites list: ${data}`);
|
||||
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, data } });
|
||||
@@ -197,9 +311,15 @@ export default async function omadaProxyHandler(req, res) {
|
||||
method: "getGlobalStat",
|
||||
}),
|
||||
headers,
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
siteResponseData = JSON.parse(data);
|
||||
siteResponseData = parseOmadaJson(data, {
|
||||
step: "global stats",
|
||||
status,
|
||||
contentType,
|
||||
url: statsUrl,
|
||||
});
|
||||
|
||||
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
|
||||
@@ -218,10 +338,17 @@ export default async function omadaProxyHandler(req, res) {
|
||||
[status, contentType, data] = await httpProxy(siteStatsUrl, {
|
||||
headers: {
|
||||
"Csrf-Token": token,
|
||||
...(omadaCookieHeader ? { Cookie: omadaCookieHeader } : {}),
|
||||
},
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
siteResponseData = JSON.parse(data);
|
||||
siteResponseData = parseOmadaJson(data, {
|
||||
step: "overview stats",
|
||||
status,
|
||||
contentType,
|
||||
url: siteStatsUrl,
|
||||
});
|
||||
|
||||
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
|
||||
@@ -242,9 +369,16 @@ export default async function omadaProxyHandler(req, res) {
|
||||
[status, contentType, data] = await httpProxy(alertUrl, {
|
||||
headers: {
|
||||
"Csrf-Token": token,
|
||||
...(omadaCookieHeader ? { Cookie: omadaCookieHeader } : {}),
|
||||
},
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
const alertResponseData = parseOmadaJson(data, {
|
||||
step: "alerts",
|
||||
status,
|
||||
contentType,
|
||||
url: alertUrl,
|
||||
});
|
||||
const alertResponseData = JSON.parse(data);
|
||||
|
||||
activeUser = siteResponseData.result.totalClientNum;
|
||||
connectedAp = siteResponseData.result.connectedApNum;
|
||||
|
||||
@@ -324,4 +324,73 @@ describe("widgets/omada/proxy", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("retries login when sites list returns HTML", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://omada", username: "u", password: "p", site: "Default" });
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({ result: { omadacId: "cid", controllerVer: "5.0.0" } }),
|
||||
])
|
||||
// initial login
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t1" } })),
|
||||
])
|
||||
// sites list unexpectedly returns HTML
|
||||
.mockResolvedValueOnce([200, "text/html;charset=utf-8", "<!DOCTYPE html><html><body>login</body></html>"])
|
||||
// retry login
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t2" } })),
|
||||
])
|
||||
// retry sites list works
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", id: "siteid" }] } }),
|
||||
])
|
||||
// overview works
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({
|
||||
errorCode: 0,
|
||||
result: {
|
||||
totalClientNum: 11,
|
||||
connectedApNum: 3,
|
||||
connectedGatewayNum: 1,
|
||||
connectedSwitchNum: 2,
|
||||
},
|
||||
}),
|
||||
])
|
||||
// alerts works
|
||||
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 5 } })]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await omadaProxyHandler(req, res);
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
"Received HTML response for Omada sites list; retrying with a fresh login.",
|
||||
);
|
||||
expect(httpProxy.mock.calls[1][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(httpProxy.mock.calls[2][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(httpProxy.mock.calls[3][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(httpProxy.mock.calls[4][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(res.body).toBe(
|
||||
JSON.stringify({
|
||||
connectedAp: 3,
|
||||
activeUser: 11,
|
||||
alerts: 5,
|
||||
connectedGateways: 1,
|
||||
connectedSwitches: 2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user