mirror of
https://github.com/gethomepage/homepage.git
synced 2026-04-01 07:42:14 -07:00
Compare commits
2 Commits
l10n_dev
...
feature/om
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48d283dc2 | ||
|
|
d313e0a124 |
@@ -6,6 +6,40 @@ const proxyName = "omadaProxyHandler";
|
|||||||
|
|
||||||
const logger = createLogger(proxyName);
|
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) {
|
async function login(loginUrl, username, password, controllerVersionMajor) {
|
||||||
const params = {
|
const params = {
|
||||||
username,
|
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",
|
method: "POST",
|
||||||
|
cookieHeader: "X-Bypass-Cookie",
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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) {
|
export default async function omadaProxyHandler(req, res) {
|
||||||
@@ -86,12 +122,18 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [loginStatus, loginResponseData] = await login(
|
const [loginStatus, loginContentType, loginData, loginCookieHeader] = await login(
|
||||||
loginUrl,
|
loginUrl,
|
||||||
widget.username,
|
widget.username,
|
||||||
widget.password,
|
widget.password,
|
||||||
controllerVersionMajor,
|
controllerVersionMajor,
|
||||||
);
|
);
|
||||||
|
const loginResponseData = parseOmadaJson(loginData, {
|
||||||
|
step: "login",
|
||||||
|
status: loginStatus,
|
||||||
|
contentType: loginContentType,
|
||||||
|
url: loginUrl,
|
||||||
|
});
|
||||||
|
|
||||||
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
|
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
|
||||||
return res
|
return res
|
||||||
@@ -100,11 +142,13 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { token } = loginResponseData.result;
|
const { token } = loginResponseData.result;
|
||||||
|
let omadaCookieHeader = loginCookieHeader;
|
||||||
|
|
||||||
let sitesUrl;
|
let sitesUrl;
|
||||||
let body = {};
|
let body = {};
|
||||||
let params = { token };
|
let params = { token };
|
||||||
let headers = { "Csrf-Token": token };
|
let headers = { "Csrf-Token": token };
|
||||||
|
if (omadaCookieHeader) headers.Cookie = omadaCookieHeader;
|
||||||
let method = "GET";
|
let method = "GET";
|
||||||
|
|
||||||
switch (controllerVersionMajor) {
|
switch (controllerVersionMajor) {
|
||||||
@@ -134,9 +178,72 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
params,
|
params,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers,
|
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) {
|
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||||
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
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" };
|
headers = { "Content-Type": "application/json" };
|
||||||
|
if (omadaCookieHeader) headers.Cookie = omadaCookieHeader;
|
||||||
params = { token };
|
params = { token };
|
||||||
|
|
||||||
[status, contentType, data] = await httpProxy(switchUrl, {
|
[status, contentType, data] = await httpProxy(switchUrl, {
|
||||||
@@ -181,9 +289,15 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
params,
|
params,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers,
|
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) {
|
if (status !== 200 || switchResponseData.errorCode > 0) {
|
||||||
logger.error(`HTTP ${status} getting sites list: ${data}`);
|
logger.error(`HTTP ${status} getting sites list: ${data}`);
|
||||||
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, 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",
|
method: "getGlobalStat",
|
||||||
}),
|
}),
|
||||||
headers,
|
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) {
|
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||||
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
|
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, {
|
[status, contentType, data] = await httpProxy(siteStatsUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
"Csrf-Token": token,
|
"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) {
|
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||||
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
|
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, {
|
[status, contentType, data] = await httpProxy(alertUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
"Csrf-Token": token,
|
"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;
|
activeUser = siteResponseData.result.totalClientNum;
|
||||||
connectedAp = siteResponseData.result.connectedApNum;
|
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