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";
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
const BASE_URL = "https://tv.odido.nl";
|
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 =
|
export const ODIDO_USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
"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(). */
|
/** Fallback als er geen username is; liever deviceIdForUsername(). */
|
||||||
const DEFAULT_DEVICE_ID = "MzkzNjIwMTU1NjAxMDNydA==";
|
const DEFAULT_DEVICE_ID = "MzkzNjIwMTU1NjAxMDNydA==";
|
||||||
const DEFAULT_SESSION_CACHE_MS = 20 * 60_000;
|
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. */
|
/** Stabiele, per-account device-id zodat sessies elkaar niet wegtrappen. */
|
||||||
export function deviceIdForUsername(username: string): string {
|
export function deviceIdForUsername(username: string): string {
|
||||||
const hash = createHash("sha256")
|
const hash = createHash("sha256")
|
||||||
|
|
@ -87,14 +125,13 @@ async function post(
|
||||||
): Promise<{ body: Record<string, unknown>; cookies: OdidoCookies }> {
|
): Promise<{ body: Record<string, unknown>; cookies: OdidoCookies }> {
|
||||||
const response = await fetch(`${BASE_URL}${path}?from=${from}`, {
|
const response = await fetch(`${BASE_URL}${path}?from=${from}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: odidoBrowserHeaders({
|
||||||
"Content-Type": "text/plain;charset=UTF-8",
|
"Content-Type": "text/plain;charset=UTF-8",
|
||||||
Origin: BASE_URL,
|
Origin: BASE_URL,
|
||||||
Referer: `${BASE_URL}/inloggen/index.html`,
|
Referer: `${BASE_URL}/inloggen/index.html`,
|
||||||
"User-Agent": ODIDO_USER_AGENT,
|
|
||||||
...(cookies?.CSRFSESSION ? { X_CSRFToken: cookies.CSRFSESSION } : {}),
|
...(cookies?.CSRFSESSION ? { X_CSRFToken: cookies.CSRFSESSION } : {}),
|
||||||
...(cookies ? { Cookie: cookieHeader(cookies) } : {}),
|
...(cookies ? { Cookie: cookieHeader(cookies) } : {}),
|
||||||
},
|
}),
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
signal: AbortSignal.timeout(path.includes("QueryAllChannel") ? 60_000 : 30_000),
|
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]
|
contentId: [body.contentID, body.contentId, trigger?.contentID, trigger?.contentId]
|
||||||
.find((value): value is string => typeof value === "string" && !!value) ?? null,
|
.find((value): value is string => typeof value === "string" && !!value) ?? null,
|
||||||
cookies: refreshedCookies,
|
cookies: refreshedCookies,
|
||||||
userAgent: ODIDO_USER_AGENT,
|
userAgent: getOdidoUserAgent(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,10 +306,12 @@ export async function getPssh(
|
||||||
cookies: OdidoCookies = {}
|
cookies: OdidoCookies = {}
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const response = await fetch(mpdUrl, {
|
const response = await fetch(mpdUrl, {
|
||||||
headers: {
|
headers: odidoBrowserHeaders({
|
||||||
"User-Agent": ODIDO_USER_AGENT,
|
Accept: "*/*",
|
||||||
|
Referer: `${BASE_URL}/`,
|
||||||
|
Origin: BASE_URL,
|
||||||
...(Object.keys(cookies).length ? { Cookie: cookieHeader(cookies) } : {}),
|
...(Object.keys(cookies).length ? { Cookie: cookieHeader(cookies) } : {}),
|
||||||
},
|
}),
|
||||||
signal: AbortSignal.timeout(30_000),
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`Odido MPD ophalen mislukt (${response.status})`);
|
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 { encryptSecret } from "../security/crypto";
|
||||||
import { extractOdidoKeys } from "./cdm";
|
import { extractOdidoKeys } from "./cdm";
|
||||||
import {
|
import {
|
||||||
|
getOdidoUserAgent,
|
||||||
getPssh,
|
getPssh,
|
||||||
login,
|
login,
|
||||||
ODIDO_USER_AGENT,
|
|
||||||
playChannel,
|
playChannel,
|
||||||
type OdidoCookies,
|
type OdidoCookies,
|
||||||
type OdidoPlayResult,
|
type OdidoPlayResult,
|
||||||
|
|
@ -50,7 +50,11 @@ async function probeMpd(url: string, timeoutMs: number, cookies?: OdidoCookies):
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
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
|
...(cookies && Object.keys(cookies).length
|
||||||
? {
|
? {
|
||||||
Cookie: Object.entries(cookies)
|
Cookie: Object.entries(cookies)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue