mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-11 22:56:34 -07:00
Chore: prevent some unnecessary failed requests from stale cookies (#6960)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import getServiceWidget from "utils/config/service-helpers";
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
|
import { setCookieHeader } from "utils/proxy/cookie-jar";
|
||||||
import { httpProxy } from "utils/proxy/http";
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
|
||||||
const logger = createLogger("floodProxyHandler");
|
const logger = createLogger("floodProxyHandler");
|
||||||
@@ -53,6 +54,8 @@ export default async function floodProxyHandler(req, res) {
|
|||||||
return res.status(status).end(data);
|
return res.status(status).end(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refresh the cookie header from the jar, otherwise the retry reuses the stale session cookie
|
||||||
|
setCookieHeader(url, params, { overwrite: true });
|
||||||
[status, contentType, data] = await httpProxy(url, params);
|
[status, contentType, data] = await httpProxy(url, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { createServer } from "node:http";
|
||||||
|
|
||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import createMockRes from "test-utils/create-mock-res";
|
||||||
|
|
||||||
|
const { getServiceWidget, logger } = vi.hoisted(() => ({
|
||||||
|
getServiceWidget: vi.fn(),
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/logger", () => ({
|
||||||
|
default: () => logger,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/config/service-helpers", () => ({
|
||||||
|
default: getServiceWidget,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import floodProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
// Integration test using the real httpProxy and cookie jar against a mock
|
||||||
|
// flood server, so session cookie handling across login/retry is exercised.
|
||||||
|
describe("widgets/flood/proxy session cookies", () => {
|
||||||
|
let server;
|
||||||
|
let validJwt = null;
|
||||||
|
let jwtCounter = 0;
|
||||||
|
|
||||||
|
const getJwt = (req) => /jwt=([^;]+)/.exec(req.headers.cookie ?? "")?.[1];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = createServer((req, res) => {
|
||||||
|
if (req.method === "POST" && req.url === "/api/auth/authenticate") {
|
||||||
|
jwtCounter += 1;
|
||||||
|
validJwt = `jwt-${jwtCounter}`;
|
||||||
|
res.writeHead(200, { "Set-Cookie": `jwt=${validJwt}; HttpOnly; path=/`, "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ success: true }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && req.url === "/api/torrents") {
|
||||||
|
if (validJwt && getJwt(req) === validJwt) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ torrents: {} }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Unauthorized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
server.listen(0, "127.0.0.1", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
type: "flood",
|
||||||
|
url: `http://127.0.0.1:${server.address().port}`,
|
||||||
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const callHandler = async () => {
|
||||||
|
const req = { query: { group: "g", service: "svc", endpoint: "torrents", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
await floodProxyHandler(req, res);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
it("logs in and returns data when no session exists yet", async () => {
|
||||||
|
const res = await callHandler();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recovers when the cached session cookie has expired server-side", async () => {
|
||||||
|
// session died server-side (timeout/restart) but the jar still holds the old cookie
|
||||||
|
validJwt = null;
|
||||||
|
|
||||||
|
const res = await callHandler();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import getServiceWidget from "utils/config/service-helpers";
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
|
import { setCookieHeader } from "utils/proxy/cookie-jar";
|
||||||
import { httpProxy } from "utils/proxy/http";
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
|
||||||
const logger = createLogger("qbittorrentProxyHandler");
|
const logger = createLogger("qbittorrentProxyHandler");
|
||||||
@@ -52,6 +53,8 @@ export default async function qbittorrentProxyHandler(req, res) {
|
|||||||
return res.status(401).end(data);
|
return res.status(401).end(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refresh the cookie header from the jar, otherwise the retry reuses the stale session cookie
|
||||||
|
setCookieHeader(url, params, { overwrite: true });
|
||||||
[status, contentType, data] = await httpProxy(url, params);
|
[status, contentType, data] = await httpProxy(url, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { createServer } from "node:http";
|
||||||
|
|
||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import createMockRes from "test-utils/create-mock-res";
|
||||||
|
|
||||||
|
const { getServiceWidget, logger } = vi.hoisted(() => ({
|
||||||
|
getServiceWidget: vi.fn(),
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/logger", () => ({
|
||||||
|
default: () => logger,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/config/service-helpers", () => ({
|
||||||
|
default: getServiceWidget,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import qbittorrentProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
// Integration test using the real httpProxy and cookie jar against a mock
|
||||||
|
// qBittorrent server, so session cookie handling across login/retry is exercised.
|
||||||
|
describe("widgets/qbittorrent/proxy session cookies", () => {
|
||||||
|
let server;
|
||||||
|
let validSid = null;
|
||||||
|
let sidCounter = 0;
|
||||||
|
|
||||||
|
const getSid = (req) => /SID=([^;]+)/.exec(req.headers.cookie ?? "")?.[1];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = createServer((req, res) => {
|
||||||
|
if (req.method === "POST" && req.url === "/api/v2/auth/login") {
|
||||||
|
sidCounter += 1;
|
||||||
|
validSid = `sid-${sidCounter}`;
|
||||||
|
res.writeHead(200, { "Set-Cookie": `SID=${validSid}; HttpOnly; path=/`, "Content-Type": "text/plain" });
|
||||||
|
res.end("Ok.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && req.url === "/api/v2/torrents/info") {
|
||||||
|
if (validSid && getSid(req) === validSid) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end("[]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
server.listen(0, "127.0.0.1", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
getServiceWidget.mockResolvedValue({
|
||||||
|
type: "qbittorrent",
|
||||||
|
url: `http://127.0.0.1:${server.address().port}`,
|
||||||
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const callHandler = async () => {
|
||||||
|
const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
await qbittorrentProxyHandler(req, res);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
it("logs in and returns data when no session exists yet", async () => {
|
||||||
|
const res = await callHandler();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recovers when the cached session cookie has expired server-side", async () => {
|
||||||
|
// session died server-side (timeout/restart) but the jar still holds the old cookie
|
||||||
|
validSid = null;
|
||||||
|
|
||||||
|
const res = await callHandler();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user