-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathuser-agent.ts
More file actions
119 lines (113 loc) · 4.65 KB
/
Copy pathuser-agent.ts
File metadata and controls
119 lines (113 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { join } from "node:path";
import { type Session, session } from "electron";
import {
chromeMajorVersion,
clientHintsBrandList,
clientHintsPlatform,
formatBrandVersionList,
} from "shared/client-hints";
const BROWSER_PARTITION = "persist:superset";
/**
* The frozen platform token real desktop Chrome sends per OS. Chrome stopped
* exposing the actual OS version in the UA (macOS is pinned to 10_15_7 even on
* Apple Silicon, Windows to NT 10.0), so these literals are what an ordinary
* Chrome install reports today.
*/
function chromePlatformToken(): string {
switch (process.platform) {
case "darwin":
return "Macintosh; Intel Mac OS X 10_15_7";
case "win32":
return "Windows NT 10.0; Win64; x64";
default:
return "X11; Linux x86_64";
}
}
/**
* A user-agent string matching what real desktop Chrome sends on this OS for
* the Chromium bundled with this Electron build. The major version comes from
* `process.versions.chrome` — never hardcode one: Client Hints (`sec-ch-ua`,
* `navigator.userAgentData`) are generated by the engine itself, and a UA
* claiming a different version than the hints report is a stronger bot signal
* than no override at all. Real Chrome's UA is version-reduced to
* `<major>.0.0.0` (the full build number only appears in Client Hints), so
* ours is too.
*/
export function buildChromeUserAgent(): string {
const chromeMajor = chromeMajorVersion(process.versions.chrome);
return `Mozilla/5.0 (${chromePlatformToken()}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeMajor}.0.0.0 Safari/537.36`;
}
/**
* Rewrite the `sec-ch-ua*` request headers to match the UA string.
* `setUserAgent()` doesn't touch Chromium's UserAgentMetadata, which generates
* these headers independently — and Electron's brand list has "Chromium" but
* no "Google Chrome" entry, so without this every request carries Client
* Hints contradicting the UA: a stronger bot signal than the stock Electron
* UA ever was.
*/
function configureClientHintHeaders(browserSession: Session): void {
const chromeVersion = process.versions.chrome;
// Real Chrome sends the low-entropy hints on every request to a secure
// origin; the full-version ones only after a site opts in via Accept-CH,
// which Chromium tracks for us — so those are only replaced when the
// engine chose to send them.
const lowEntropyHeaders: Record<string, string> = {
"sec-ch-ua": formatBrandVersionList(
clientHintsBrandList(chromeVersion, "major"),
),
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": `"${clientHintsPlatform(process.platform)}"`,
};
const optInHeaders: Record<string, string> = {
"sec-ch-ua-full-version-list": formatBrandVersionList(
clientHintsBrandList(chromeVersion, "full"),
),
"sec-ch-ua-full-version": `"${chromeMajorVersion(chromeVersion)}.0.0.0"`,
};
// Electron keeps a single onBeforeSendHeaders listener per session — a
// second registration elsewhere would silently replace this one.
browserSession.webRequest.onBeforeSendHeaders((details, callback) => {
const headers = details.requestHeaders;
const present = new Set<string>();
for (const name of Object.keys(headers)) {
const lower = name.toLowerCase();
if (
Object.hasOwn(lowEntropyHeaders, lower) ||
Object.hasOwn(optInHeaders, lower)
) {
present.add(lower);
delete headers[name];
}
}
const secureOrigin =
details.url.startsWith("https:") || details.url.startsWith("wss:");
for (const [name, value] of Object.entries(lowEntropyHeaders)) {
if (secureOrigin || present.has(name)) headers[name] = value;
}
for (const [name, value] of Object.entries(optInHeaders)) {
if (present.has(name)) headers[name] = value;
}
callback({ requestHeaders: headers });
});
}
/**
* Make the browser pane's session fingerprint as ordinary Chrome, in all
* three places bot-detection scripts (Cloudflare, DataDome, PerimeterX, …)
* cross-check: the UA string, the `sec-ch-ua*` request headers, and the
* `navigator.userAgentData` object page JS reads. All are derived from the
* same `process.versions.chrome`, so they can never disagree. A mismatch
* between any two can get a user's own ordinary browsing wrongly challenged
* or blocked.
*/
export function configureBrowserUserAgent(): void {
const browserSession = session.fromPartition(BROWSER_PARTITION);
browserSession.setUserAgent(buildChromeUserAgent());
configureClientHintHeaders(browserSession);
// Headers alone don't change what live scripts read from
// navigator.userAgentData; a session preload rebuilds it in every frame
// before page scripts run.
browserSession.registerPreloadScript({
type: "frame",
filePath: join(__dirname, "../preload/browser-client-hints.js"),
});
}