fix: fetch XSRF token from /antiforgery/token before POST
This commit is contained in:
@@ -19,7 +19,6 @@
|
|||||||
"@solana/spl-token": "^0.4.14",
|
"@solana/spl-token": "^0.4.14",
|
||||||
"@solana/web3.js": "^1.87.6",
|
"@solana/web3.js": "^1.87.6",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-cookiejar-support": "^5.0.5",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@@ -31,7 +30,6 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"socket.io": "^4.7.3",
|
"socket.io": "^4.7.3",
|
||||||
"tough-cookie": "^5.1.2",
|
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"ws": "^8.15.1"
|
"ws": "^8.15.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
import axios, { type AxiosInstance } from "axios";
|
import axios from "axios";
|
||||||
import { wrapper } from "axios-cookiejar-support";
|
|
||||||
import { CookieJar } from "tough-cookie";
|
|
||||||
|
|
||||||
const LISTENARR_URL = process.env.LISTENARR_URL || "";
|
const LISTENARR_URL = process.env.LISTENARR_URL || "";
|
||||||
const LISTENARR_API_KEY = process.env.LISTENARR_API_KEY || "";
|
const LISTENARR_API_KEY = process.env.LISTENARR_API_KEY || "";
|
||||||
|
|
||||||
// ponytail: single shared cookie jar so CSRF cookies persist across requests
|
const listenarrApi = axios.create({
|
||||||
const jar = new CookieJar();
|
baseURL: LISTENARR_URL,
|
||||||
const listenarrApi: AxiosInstance = wrapper(
|
timeout: 15000,
|
||||||
axios.create({
|
headers: {
|
||||||
baseURL: LISTENARR_URL,
|
"Content-Type": "application/json",
|
||||||
timeout: 15000,
|
...(LISTENARR_API_KEY ? { "X-Api-Key": LISTENARR_API_KEY } : {}),
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
});
|
||||||
...(LISTENARR_API_KEY ? { "X-Api-Key": LISTENARR_API_KEY } : {}),
|
|
||||||
},
|
|
||||||
withCredentials: true,
|
|
||||||
jar,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ponytail: prime the antiforgery cookie on module load
|
// ponytail: fetch CSRF token from Listenarr's /antiforgery/token endpoint
|
||||||
let csrfPrimed = false;
|
let cachedCsrfToken: string | null = null;
|
||||||
async function ensureCsrfCookie(): Promise<void> {
|
async function getCsrfToken(): Promise<string | null> {
|
||||||
if (csrfPrimed || LISTENARR_API_KEY) return;
|
if (cachedCsrfToken) return cachedCsrfToken;
|
||||||
try {
|
try {
|
||||||
await listenarrApi.get("/api/v1/system/status");
|
const resp = await listenarrApi.get("/api/v1/antiforgery/token");
|
||||||
csrfPrimed = true;
|
cachedCsrfToken = resp.data?.token || null;
|
||||||
|
return cachedCsrfToken;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore — we just need the cookie set
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +55,6 @@ export async function addAudiobook(options: {
|
|||||||
explicit?: boolean;
|
explicit?: boolean;
|
||||||
abridged?: boolean;
|
abridged?: boolean;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
await ensureCsrfCookie();
|
|
||||||
|
|
||||||
// Defensive: ensure series/seriesNumber are always strings, not arrays
|
// Defensive: ensure series/seriesNumber are always strings, not arrays
|
||||||
const series = typeof options.series === "string" ? options.series : "";
|
const series = typeof options.series === "string" ? options.series : "";
|
||||||
const seriesNumber = typeof options.seriesNumber === "string" ? options.seriesNumber : "";
|
const seriesNumber = typeof options.seriesNumber === "string" ? options.seriesNumber : "";
|
||||||
@@ -94,8 +85,25 @@ export async function addAudiobook(options: {
|
|||||||
qualityProfileId: 1,
|
qualityProfileId: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resp = await listenarrApi.post("/api/v1/library/add", body);
|
const csrfToken = await getCsrfToken();
|
||||||
return resp.data;
|
const headers: Record<string, string> = {};
|
||||||
|
if (csrfToken) headers["X-XSRF-TOKEN"] = csrfToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await listenarrApi.post("/api/v1/library/add", body, { headers });
|
||||||
|
return resp.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
// ponytail: retry once on CSRF failure after refreshing token
|
||||||
|
if (err?.response?.status === 400 && /csrf|xsrf|antiforgery/i.test(JSON.stringify(err?.response?.data))) {
|
||||||
|
cachedCsrfToken = null;
|
||||||
|
const freshToken = await getCsrfToken();
|
||||||
|
if (freshToken) {
|
||||||
|
const retryResp = await listenarrApi.post("/api/v1/library/add", body, { headers: { "X-XSRF-TOKEN": freshToken } });
|
||||||
|
return retryResp.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getListenarrStatus(): Promise<{
|
export async function getListenarrStatus(): Promise<{
|
||||||
|
|||||||
Reference in New Issue
Block a user