Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/plugins/default-browser-emulator/lib/helpers/modifyHeaders.ts
1030 views
1
import IResourceHeaders from '@secret-agent/interfaces/IResourceHeaders';
2
import { pickRandom } from '@secret-agent/commons/utils';
3
import IHttpResourceLoadDetails from '@secret-agent/interfaces/IHttpResourceLoadDetails';
4
import BrowserEmulator from '../../index';
5
import IBrowserData, { IDataHeaderOrder, IDataHeaders } from '../../interfaces/IBrowserData';
6
7
export default function modifyHeaders(
8
browserEmulator: BrowserEmulator,
9
data: IBrowserData,
10
resource: IHttpResourceLoadDetails,
11
) {
12
const { userAgentString, locale } = browserEmulator;
13
const defaultOrder = getResourceHeaderDefaults(browserEmulator, data.headers, resource);
14
const headers = resource.requestHeaders;
15
16
// if no default order, at least ensure connection and user-agent
17
if (!defaultOrder) {
18
const newHeaders: IResourceHeaders = {};
19
let hasKeepAlive = false;
20
for (const [header, value] of Object.entries(headers)) {
21
const lower = toLowerCase(header);
22
if (lower === 'connection') hasKeepAlive = true;
23
24
if (lower === 'user-agent') {
25
newHeaders[header] = userAgentString;
26
} else {
27
newHeaders[header] = value;
28
}
29
}
30
if (!hasKeepAlive && !resource.isServerHttp2) {
31
newHeaders.Connection = 'keep-alive';
32
}
33
34
resource.requestHeaders = newHeaders;
35
36
return true;
37
}
38
39
const isXhr = resource.resourceType === 'Fetch' || resource.resourceType === 'Xhr';
40
41
const requestLowerHeaders = {};
42
for (const [key, value] of Object.entries(resource.requestHeaders)) {
43
requestLowerHeaders[toLowerCase(key)] = value;
44
}
45
46
// First add headers in the default order
47
48
const headerList: [string, string | string[]][] = [];
49
for (const headerName of defaultOrder.order) {
50
const defaults = defaultOrder.defaults[headerName];
51
const lowerName = toLowerCase(headerName);
52
let value = requestLowerHeaders[lowerName];
53
54
if (lowerName === 'accept-language') {
55
value = `${locale};q=0.9`;
56
// if header is an Sec- header, trust Chrome
57
} else if (value && lowerName.startsWith('sec-')) {
58
// keep given value
59
} else if (value && lowerName === 'accept' && isXhr) {
60
// allow user to customize accept value on fetch/xhr
61
} else if (lowerName === 'user-agent') {
62
value = userAgentString;
63
} else if (defaults && !defaults.includes(value as string)) {
64
value = pickRandom(defaults);
65
}
66
67
if (value) {
68
headerList.push([headerName, value]);
69
}
70
}
71
72
// Now go through and add any custom headers
73
let index = -1;
74
for (const [header, value] of Object.entries(headers)) {
75
index += 1;
76
const lowerHeader = toLowerCase(header);
77
const isAlreadyIncluded = defaultOrder.orderKeys.has(lowerHeader);
78
if (isAlreadyIncluded) continue;
79
80
// if past the end, reset the index to the last spot
81
if (index >= headerList.length) index = headerList.length - 1;
82
83
// insert at same index it would have been otherwise (unless past end)
84
headerList.splice(index, 0, [header, value]);
85
}
86
87
const newHeaders: IResourceHeaders = {};
88
for (const entry of headerList) newHeaders[entry[0]] = entry[1];
89
90
resource.requestHeaders = newHeaders;
91
return true;
92
}
93
94
function getResourceHeaderDefaults(
95
browserEmulator: BrowserEmulator,
96
headerProfiles: IDataHeaders,
97
resource: IHttpResourceLoadDetails,
98
): Pick<IDataHeaderOrder, 'order' | 'orderKeys' | 'defaults'> {
99
const { method, originType, requestHeaders: headers, resourceType, isSSL } = resource;
100
101
let protocol = resource.isServerHttp2 ? 'http2' : 'https';
102
if (!resource.isSSL) protocol = 'http';
103
104
let profiles = headerProfiles[protocol][resourceType];
105
if (!profiles && resourceType === 'Websocket') {
106
profiles = headerProfiles[protocol].WebsocketUpgrade;
107
}
108
if (!profiles) return null;
109
110
for (const defaultOrder of profiles) {
111
defaultOrder.orderKeys ??= new Set(defaultOrder.order.map(toLowerCase));
112
}
113
114
let defaultOrders = profiles.filter(x => x.method === method);
115
116
if (defaultOrders.length > 1) {
117
const filtered = defaultOrders.filter(x => x.originTypes.includes(originType));
118
if (filtered.length) defaultOrders = filtered;
119
}
120
121
if (defaultOrders.length > 1 && (headers['sec-fetch-user'] || headers['Sec-Fetch-User'])) {
122
const filtered = defaultOrders.filter(x => x.orderKeys.has('sec-fetch-user'));
123
if (filtered.length) defaultOrders = filtered;
124
}
125
126
if (defaultOrders.length > 1) {
127
if (headers.Cookie || headers.cookie) {
128
const filtered = defaultOrders.filter(x => x.orderKeys.has('cookie'));
129
if (filtered.length) defaultOrders = filtered;
130
}
131
}
132
133
const defaultOrder = defaultOrders.length ? pickRandom(defaultOrders) : null;
134
135
if (!defaultOrder) {
136
browserEmulator.logger.info('Headers.NotFound', { resourceType, isSSL, method, originType });
137
return null;
138
}
139
140
return defaultOrder;
141
}
142
143
const lowerCaseMap = new Map<string, string>();
144
145
function toLowerCase(header: string): string {
146
if (!lowerCaseMap.has(header)) lowerCaseMap.set(header, header.toLowerCase());
147
return lowerCaseMap.get(header);
148
}
149
150