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:
Jos Vooges | STH 2026-09-26 04:37:19 +02:00
parent 1ef0c4339f
commit c58f9c423b
2 changed files with 53 additions and 10 deletions

View file

@ -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})`);

View file

@ -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)