Path: blob/main/plugins/default-browser-emulator/lib/helpers/modifyHeaders.ts
1030 views
import IResourceHeaders from '@secret-agent/interfaces/IResourceHeaders';1import { pickRandom } from '@secret-agent/commons/utils';2import IHttpResourceLoadDetails from '@secret-agent/interfaces/IHttpResourceLoadDetails';3import BrowserEmulator from '../../index';4import IBrowserData, { IDataHeaderOrder, IDataHeaders } from '../../interfaces/IBrowserData';56export default function modifyHeaders(7browserEmulator: BrowserEmulator,8data: IBrowserData,9resource: IHttpResourceLoadDetails,10) {11const { userAgentString, locale } = browserEmulator;12const defaultOrder = getResourceHeaderDefaults(browserEmulator, data.headers, resource);13const headers = resource.requestHeaders;1415// if no default order, at least ensure connection and user-agent16if (!defaultOrder) {17const newHeaders: IResourceHeaders = {};18let hasKeepAlive = false;19for (const [header, value] of Object.entries(headers)) {20const lower = toLowerCase(header);21if (lower === 'connection') hasKeepAlive = true;2223if (lower === 'user-agent') {24newHeaders[header] = userAgentString;25} else {26newHeaders[header] = value;27}28}29if (!hasKeepAlive && !resource.isServerHttp2) {30newHeaders.Connection = 'keep-alive';31}3233resource.requestHeaders = newHeaders;3435return true;36}3738const isXhr = resource.resourceType === 'Fetch' || resource.resourceType === 'Xhr';3940const requestLowerHeaders = {};41for (const [key, value] of Object.entries(resource.requestHeaders)) {42requestLowerHeaders[toLowerCase(key)] = value;43}4445// First add headers in the default order4647const headerList: [string, string | string[]][] = [];48for (const headerName of defaultOrder.order) {49const defaults = defaultOrder.defaults[headerName];50const lowerName = toLowerCase(headerName);51let value = requestLowerHeaders[lowerName];5253if (lowerName === 'accept-language') {54value = `${locale};q=0.9`;55// if header is an Sec- header, trust Chrome56} else if (value && lowerName.startsWith('sec-')) {57// keep given value58} else if (value && lowerName === 'accept' && isXhr) {59// allow user to customize accept value on fetch/xhr60} else if (lowerName === 'user-agent') {61value = userAgentString;62} else if (defaults && !defaults.includes(value as string)) {63value = pickRandom(defaults);64}6566if (value) {67headerList.push([headerName, value]);68}69}7071// Now go through and add any custom headers72let index = -1;73for (const [header, value] of Object.entries(headers)) {74index += 1;75const lowerHeader = toLowerCase(header);76const isAlreadyIncluded = defaultOrder.orderKeys.has(lowerHeader);77if (isAlreadyIncluded) continue;7879// if past the end, reset the index to the last spot80if (index >= headerList.length) index = headerList.length - 1;8182// insert at same index it would have been otherwise (unless past end)83headerList.splice(index, 0, [header, value]);84}8586const newHeaders: IResourceHeaders = {};87for (const entry of headerList) newHeaders[entry[0]] = entry[1];8889resource.requestHeaders = newHeaders;90return true;91}9293function getResourceHeaderDefaults(94browserEmulator: BrowserEmulator,95headerProfiles: IDataHeaders,96resource: IHttpResourceLoadDetails,97): Pick<IDataHeaderOrder, 'order' | 'orderKeys' | 'defaults'> {98const { method, originType, requestHeaders: headers, resourceType, isSSL } = resource;99100let protocol = resource.isServerHttp2 ? 'http2' : 'https';101if (!resource.isSSL) protocol = 'http';102103let profiles = headerProfiles[protocol][resourceType];104if (!profiles && resourceType === 'Websocket') {105profiles = headerProfiles[protocol].WebsocketUpgrade;106}107if (!profiles) return null;108109for (const defaultOrder of profiles) {110defaultOrder.orderKeys ??= new Set(defaultOrder.order.map(toLowerCase));111}112113let defaultOrders = profiles.filter(x => x.method === method);114115if (defaultOrders.length > 1) {116const filtered = defaultOrders.filter(x => x.originTypes.includes(originType));117if (filtered.length) defaultOrders = filtered;118}119120if (defaultOrders.length > 1 && (headers['sec-fetch-user'] || headers['Sec-Fetch-User'])) {121const filtered = defaultOrders.filter(x => x.orderKeys.has('sec-fetch-user'));122if (filtered.length) defaultOrders = filtered;123}124125if (defaultOrders.length > 1) {126if (headers.Cookie || headers.cookie) {127const filtered = defaultOrders.filter(x => x.orderKeys.has('cookie'));128if (filtered.length) defaultOrders = filtered;129}130}131132const defaultOrder = defaultOrders.length ? pickRandom(defaultOrders) : null;133134if (!defaultOrder) {135browserEmulator.logger.info('Headers.NotFound', { resourceType, isSSL, method, originType });136return null;137}138139return defaultOrder;140}141142const lowerCaseMap = new Map<string, string>();143144function toLowerCase(header: string): string {145if (!lowerCaseMap.has(header)) lowerCaseMap.set(header, header.toLowerCase());146return lowerCaseMap.get(header);147}148149150