Skip to content

Commit 8ea5c20

Browse files
committed
fix: bind BotGuard to YouTube page context
1 parent beb3ad9 commit 8ea5c20

6 files changed

Lines changed: 58 additions & 51 deletions

File tree

‎src/botguard-challenge.ts‎

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Constants } from "youtubei.js";
22
import { youtubeFetch } from "./youtube-fetch.ts";
3+
import { parseYoutubePageAttestation } from "./youtube-page-attestation.ts";
34

45
export const WEB_CLIENT_VERSION = Constants.CLIENTS.WEB.VERSION;
5-
const ATT_GET_URL = "https://www.youtube.com/youtubei/v1/att/get?prettyPrint=false";
6-
const WAA_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw";
7-
const USER_AGENT =
6+
export const WEB_USER_AGENT =
87
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
98
"Chrome/131.0.0.0 Safari/537.3";
9+
const YOUTUBE_HOME_URL = "https://www.youtube.com/";
1010

1111
type AttGetChallenge = {
1212
interpreterJavascript?: {
@@ -23,6 +23,7 @@ export type BotGuardChallenge = {
2323
interpreterScript: string;
2424
program: string;
2525
globalName: string;
26+
eventId: string;
2627
};
2728

2829
async function resolveInterpreterScript(challenge: AttGetChallenge): Promise<string> {
@@ -51,46 +52,29 @@ async function resolveInterpreterScript(challenge: AttGetChallenge): Promise<str
5152
}
5253

5354
export async function fetchChallenge(visitorData: string): Promise<BotGuardChallenge> {
54-
const response = await youtubeFetch(ATT_GET_URL, {
55-
method: "POST",
55+
const response = await youtubeFetch(YOUTUBE_HOME_URL, {
5656
headers: {
57-
"Content-Type": "application/json",
58-
"User-Agent": USER_AGENT,
59-
Accept: "application/json",
57+
"User-Agent": WEB_USER_AGENT,
58+
Accept: "text/html,application/xhtml+xml",
59+
"Accept-Language": "en-US,en;q=0.7",
6060
"X-Goog-Visitor-Id": visitorData,
61-
"X-Youtube-Client-Name": "1",
62-
"X-Youtube-Client-Version": WEB_CLIENT_VERSION,
63-
"x-goog-api-key": WAA_API_KEY,
64-
"x-user-agent": "grpc-web-javascript/0.1",
6561
},
66-
body: JSON.stringify({
67-
engagementType: "ENGAGEMENT_TYPE_UNBOUND",
68-
context: {
69-
client: {
70-
clientName: "WEB",
71-
clientVersion: WEB_CLIENT_VERSION,
72-
hl: "en",
73-
gl: "US",
74-
utcOffsetMinutes: 0,
75-
visitorData,
76-
},
77-
},
78-
}),
7962
});
8063

8164
if (!response.ok) {
82-
throw new Error(`att/get request failed: ${response.status}`);
65+
throw new Error(`YouTube page request failed: ${response.status}`);
8366
}
8467

85-
const data = (await response.json()) as { bgChallenge?: AttGetChallenge };
68+
const page = parseYoutubePageAttestation(await response.text());
69+
const data = JSON.parse(page.rawChallenge) as { bgChallenge?: AttGetChallenge };
8670
const challenge = data.bgChallenge;
8771

8872
if (
8973
!challenge ||
9074
typeof challenge.program !== "string" ||
9175
typeof challenge.globalName !== "string"
9276
) {
93-
throw new Error("att/get response missing a usable BotGuard challenge");
77+
throw new Error("YouTube page has no usable BotGuard challenge");
9478
}
9579

9680
const interpreterScript = await resolveInterpreterScript(challenge);
@@ -99,5 +83,6 @@ export async function fetchChallenge(visitorData: string): Promise<BotGuardChall
9983
interpreterScript,
10084
program: challenge.program,
10185
globalName: challenge.globalName,
86+
eventId: page.eventId,
10287
};
10388
}

‎src/botguard-page.ts‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import type { Browser, Page } from "playwright";
22
import { chromium } from "playwright";
3+
import { WEB_USER_AGENT } from "./botguard-challenge.ts";
34

45
const ROUTE = "https://www.youtube.com/__bgp__";
56
const BLANK_HTML = "<!DOCTYPE html><html><body></body></html>";
6-
const USER_AGENT =
7-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
8-
"Chrome/131.0.0.0 Safari/537.3";
97

108
let browser: Browser | null = null;
119
let page: Page | null = null;
@@ -18,7 +16,7 @@ async function ensurePage(): Promise<Page> {
1816
});
1917
}
2018
if (!page || page.isClosed()) {
21-
page = await browser.newPage({ userAgent: USER_AGENT });
19+
page = await browser.newPage({ userAgent: WEB_USER_AGENT });
2220
await page.route(ROUTE, (route) =>
2321
route.fulfill({ contentType: "text/html", body: BLANK_HTML }),
2422
);
@@ -27,7 +25,7 @@ async function ensurePage(): Promise<Page> {
2725
return page;
2826
}
2927

30-
type BotGuardArgs = { script: string; prog: string; name: string };
28+
type BotGuardArgs = { script: string; prog: string; name: string; eventId: string };
3129
type MintArgs = { token: string; id: string };
3230
type PoTokenMinter = (id: Uint8Array) => Uint8Array | Promise<Uint8Array>;
3331
type PoTokenGlobal = Record<string, unknown> & {
@@ -40,10 +38,23 @@ export async function executeBotGuard(
4038
interpreterScript: string,
4139
program: string,
4240
globalName: string,
41+
eventId: string,
4342
): Promise<string> {
4443
const p = await ensurePage();
4544
return p.evaluate(
4645
async (args: BotGuardArgs): Promise<string> => {
46+
const root = globalThis as Record<string, unknown>;
47+
let yt = root.yt as Record<string, unknown> | undefined;
48+
if (!yt) {
49+
yt = {};
50+
root.yt = yt;
51+
}
52+
let config = yt.config_ as Record<string, unknown> | undefined;
53+
if (!config) {
54+
config = {};
55+
yt.config_ = config;
56+
}
57+
config.EVENT_ID = args.eventId;
4758
new Function(args.script)();
4859
const g = globalThis as Record<string, Record<string, (...a: unknown[]) => unknown>>;
4960
const vm = g[args.name];
@@ -87,7 +98,7 @@ export async function executeBotGuard(
8798
af((r: unknown) => resolve(r as string), [undefined, undefined, wpo, undefined]);
8899
});
89100
},
90-
{ script: interpreterScript, prog: program, name: globalName },
101+
{ script: interpreterScript, prog: program, name: globalName, eventId },
91102
);
92103
}
93104

‎src/token-service.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ async function buildSession(): Promise<CachedSession> {
3333
challenge.interpreterScript,
3434
challenge.program,
3535
challenge.globalName,
36+
challenge.eventId,
3637
);
3738
const integrityTokenData = await fetchIntegrityToken(botguardResponse);
3839

‎tests/botguard-challenge.test.ts‎

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,40 @@ afterEach(() => {
88
});
99

1010
describe("fetchChallenge", () => {
11-
it("binds att/get to the current visitor session", async () => {
11+
it("binds the page-native challenge to the current visitor session", async () => {
1212
let request: Request | null = null;
13+
const rawChallenge = JSON.stringify({
14+
bgChallenge: {
15+
interpreterJavascript: {
16+
privateDoNotAccessOrElseSafeScriptWrappedValue: "interpreter",
17+
},
18+
program: "program",
19+
globalName: "trayride",
20+
},
21+
});
1322
globalThis.fetch = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
1423
request = new Request(input, init);
15-
return Response.json({
16-
bgChallenge: {
17-
interpreterJavascript: {
18-
privateDoNotAccessOrElseSafeScriptWrappedValue: "interpreter",
19-
},
20-
program: "program",
21-
globalName: "trayride",
22-
},
23-
});
24+
return new Response(
25+
`<script>ytcfg.set({"EVENT_ID":"event-123"});` +
26+
`window.ytAtN({"R":${JSON.stringify(rawChallenge)}});</script>`,
27+
);
2428
}) as typeof fetch;
2529

2630
const { fetchChallenge, WEB_CLIENT_VERSION } = await import(
2731
"../src/botguard-challenge.ts?contract-test"
2832
);
2933
const challenge = await fetchChallenge("visitor-session");
30-
const body = (await request?.json()) as {
31-
context: { client: { visitorData: string; clientVersion: string } };
32-
};
3334

3435
expect(challenge).toEqual({
3536
interpreterScript: "interpreter",
3637
program: "program",
3738
globalName: "trayride",
39+
eventId: "event-123",
3840
});
3941
expect(WEB_CLIENT_VERSION).toBe(Constants.CLIENTS.WEB.VERSION);
42+
expect(request?.url).toBe("https://www.youtube.com/");
43+
expect(request?.method).toBe("GET");
4044
expect(request?.headers.get("X-Goog-Visitor-Id")).toBe("visitor-session");
41-
expect(request?.headers.get("X-Youtube-Client-Name")).toBe("1");
42-
expect(body.context.client.visitorData).toBe("visitor-session");
43-
expect(body.context.client.clientVersion).toBe(Constants.CLIENTS.WEB.VERSION);
45+
expect(request?.headers.get("Accept-Language")).toBe("en-US,en;q=0.7");
4446
});
4547
});

‎tests/token-service-refresh.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ mock.module("../src/botguard-challenge.ts", () => ({
5656
interpreterScript: "interpreter",
5757
program: "program",
5858
globalName: "trayride",
59+
eventId: "event-id",
5960
})),
6061
}));
6162

‎tests/token-service.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const INTEGRITY_TOKEN = "integrity-token-test-xyz";
77
let currentVisitorData = VISITOR_DATA;
88

99
const mockExecuteBotGuard = mock(
10-
async (_script: string, _prog: string, _name: string): Promise<string> =>
10+
async (_script: string, _prog: string, _name: string, _eventId: string): Promise<string> =>
1111
"botguard-response-test",
1212
);
1313
const mockMintPoToken = mock(async (_token: string, id: string): Promise<string> => `pot-${id}`);
@@ -24,6 +24,7 @@ mock.module("../src/botguard-challenge.ts", () => ({
2424
interpreterScript: "/* noop */",
2525
program: "program-test",
2626
globalName: "vm_test",
27+
eventId: "event-test",
2728
})),
2829
}));
2930

@@ -66,6 +67,12 @@ describe("fetchPoToken", () => {
6667
expect(r1.streamingPot).toBe("pot-concurrent-1");
6768
expect(r2.streamingPot).toBe("pot-concurrent-2");
6869
expect(mockExecuteBotGuard.mock.calls.length).toBe(1);
70+
expect(mockExecuteBotGuard).toHaveBeenCalledWith(
71+
"/* noop */",
72+
"program-test",
73+
"vm_test",
74+
"event-test",
75+
);
6976
});
7077

7178
it("returns correct token structure on first call", async () => {

0 commit comments

Comments
 (0)