Harden Odido HTTP fingerprint: current Chrome UA plus Accept-Language and Sec-CH/Fetch headers.
Bare or outdated clients are more likely to be rejected; keep XHR-like browser headers on all VSP and MPD calls.
This commit is contained in:
parent
1ef0c4339f
commit
c58f9c423b
2 changed files with 53 additions and 10 deletions
|
|
@ -1,13 +1,51 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
const BASE_URL = "https://tv.odido.nl";
|
||||
|
||||
/**
|
||||
* Browser-achtige UA + Client Hints. Odido weigert/ruikt kale Node-fetch of
|
||||
* requests zonder User-Agent sneller; houd dit in sync met desktop Chrome.
|
||||
* Override via Scripts → Odido tweaks `userAgent` indien nodig.
|
||||
*/
|
||||
export const ODIDO_USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
"(KHTML, like Gecko) Chrome/140.0.7339.208 Safari/537.36";
|
||||
|
||||
const ODIDO_SEC_CH_UA =
|
||||
'"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"';
|
||||
|
||||
/** Fallback als er geen username is; liever deviceIdForUsername(). */
|
||||
const DEFAULT_DEVICE_ID = "MzkzNjIwMTU1NjAxMDNydA==";
|
||||
const DEFAULT_SESSION_CACHE_MS = 20 * 60_000;
|
||||
|
||||
let runtimeUserAgent = ODIDO_USER_AGENT;
|
||||
|
||||
/** Laat sync/play de (optioneel getweakte) UA zetten vóór requests. */
|
||||
export function setOdidoUserAgent(ua: string | null | undefined) {
|
||||
const next = (ua ?? "").trim();
|
||||
runtimeUserAgent = next || ODIDO_USER_AGENT;
|
||||
}
|
||||
|
||||
export function getOdidoUserAgent(): string {
|
||||
return runtimeUserAgent || ODIDO_USER_AGENT;
|
||||
}
|
||||
|
||||
function odidoBrowserHeaders(extra?: Record<string, string>): Record<string, string> {
|
||||
const ua = getOdidoUserAgent();
|
||||
return {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Accept-Language": "nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"User-Agent": ua,
|
||||
"sec-ch-ua": ODIDO_SEC_CH_UA,
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
/** Stabiele, per-account device-id zodat sessies elkaar niet wegtrappen. */
|
||||
export function deviceIdForUsername(username: string): string {
|
||||
const hash = createHash("sha256")
|
||||
|
|
@ -87,14 +125,13 @@ async function post(
|
|||
): Promise<{ body: Record<string, unknown>; cookies: OdidoCookies }> {
|
||||
const response = await fetch(`${BASE_URL}${path}?from=${from}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
headers: odidoBrowserHeaders({
|
||||
"Content-Type": "text/plain;charset=UTF-8",
|
||||
Origin: BASE_URL,
|
||||
Referer: `${BASE_URL}/inloggen/index.html`,
|
||||
"User-Agent": ODIDO_USER_AGENT,
|
||||
...(cookies?.CSRFSESSION ? { X_CSRFToken: cookies.CSRFSESSION } : {}),
|
||||
...(cookies ? { Cookie: cookieHeader(cookies) } : {}),
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(data),
|
||||
signal: AbortSignal.timeout(path.includes("QueryAllChannel") ? 60_000 : 30_000),
|
||||
});
|
||||
|
|
@ -260,7 +297,7 @@ export async function playChannel(
|
|||
contentId: [body.contentID, body.contentId, trigger?.contentID, trigger?.contentId]
|
||||
.find((value): value is string => typeof value === "string" && !!value) ?? null,
|
||||
cookies: refreshedCookies,
|
||||
userAgent: ODIDO_USER_AGENT,
|
||||
userAgent: getOdidoUserAgent(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -269,10 +306,12 @@ export async function getPssh(
|
|||
cookies: OdidoCookies = {}
|
||||
): Promise<string | null> {
|
||||
const response = await fetch(mpdUrl, {
|
||||
headers: {
|
||||
"User-Agent": ODIDO_USER_AGENT,
|
||||
headers: odidoBrowserHeaders({
|
||||
Accept: "*/*",
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
...(Object.keys(cookies).length ? { Cookie: cookieHeader(cookies) } : {}),
|
||||
},
|
||||
}),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Odido MPD ophalen mislukt (${response.status})`);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { prisma } from "../database/client";
|
|||
import { encryptSecret } from "../security/crypto";
|
||||
import { extractOdidoKeys } from "./cdm";
|
||||
import {
|
||||
getOdidoUserAgent,
|
||||
getPssh,
|
||||
login,
|
||||
ODIDO_USER_AGENT,
|
||||
playChannel,
|
||||
type OdidoCookies,
|
||||
type OdidoPlayResult,
|
||||
|
|
@ -50,7 +50,11 @@ async function probeMpd(url: string, timeoutMs: number, cookies?: OdidoCookies):
|
|||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"User-Agent": ODIDO_USER_AGENT,
|
||||
Accept: "*/*",
|
||||
"Accept-Language": "nl-NL,nl;q=0.9,en;q=0.8",
|
||||
"User-Agent": getOdidoUserAgent(),
|
||||
Referer: "https://tv.odido.nl/",
|
||||
Origin: "https://tv.odido.nl",
|
||||
...(cookies && Object.keys(cookies).length
|
||||
? {
|
||||
Cookie: Object.entries(cookies)
|
||||
|
|
|
|||
Loading…
Reference in a new issue