fingerprint-me-not
Version:
A powerful Node.js HTTP client with advanced TLS fingerprinting capabilities
201 lines • 8.9 kB
JavaScript
import { randomUUID } from 'crypto';
import { createRequire } from 'module';
import { CookieJar } from 'tough-cookie';
import { request } from './cffi.js';
import { FingerPrintMeNotException } from './exceptions.js';
import { Response } from './response.js';
// Helper function to determine if a URL likely returns binary content
function isLikelyBinaryUrl(url) {
const binaryExtensions = ['.pdf', '.zip', '.exe', '.dmg', '.pkg', '.deb', '.rpm',
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp',
'.mp4', '.avi', '.mov', '.mkv', '.mp3', '.wav', '.flac',
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
const lowerUrl = url.toLowerCase();
return binaryExtensions.some(ext => lowerUrl.includes(ext));
}
const require = createRequire(import.meta.url);
const { version } = require('../package.json');
export class Session {
sessionId;
headers = {};
proxy = '';
params = {};
cookies;
sessionOptions;
constructor(options = {}) {
this.sessionId = randomUUID();
this.headers = {
'User-Agent': `finger-print-me-not/${version}`,
'Accept-Encoding': 'gzip, deflate, br',
'Accept': '*/*',
'Connection': 'keep-alive',
};
this.proxy = '';
this.params = {};
this.cookies = new CookieJar();
this.sessionOptions = options;
}
async executeRequest(method, url, options = {}) {
if (options?.params) {
url += `?${new URLSearchParams(options.params).toString()}`;
}
let requestBody;
let contentType;
if (options?.json) {
requestBody = JSON.stringify(options.json);
contentType = 'application/json';
}
else if (options?.data) {
requestBody = new URLSearchParams(Object.entries(options.data)).toString();
contentType = 'application/x-www-form-urlencoded';
}
if (contentType) {
this.headers['Content-Type'] = contentType;
}
let headers = this.headers;
if (options?.headers) {
headers = { ...this.headers, ...options.headers };
}
let cookies = this.cookies;
if (options?.cookies) {
for (const [cookieKey, cookieValue] of Object.entries(options.cookies)) {
await this.cookies.setCookie(`${cookieKey}=${cookieValue}`, url);
}
cookies = this.cookies;
}
headers['Cookie'] = await cookies.getCookieString(url);
let proxy = this.proxy;
if (options?.proxy) {
proxy = options.proxy;
}
// Determine if this should be a binary request
let isBinaryRequest;
if (options?.isBinaryRequest !== undefined) {
// Explicitly specified by user
isBinaryRequest = options.isBinaryRequest;
}
else {
// Auto-detect based on URL or Accept header
const acceptHeader = headers['Accept'] || headers['accept'] || '';
isBinaryRequest = isLikelyBinaryUrl(url) ||
acceptHeader.includes('application/pdf') ||
acceptHeader.includes('application/octet-stream') ||
acceptHeader.includes('image/') ||
acceptHeader.includes('video/') ||
acceptHeader.includes('audio/');
}
const requestPayload = {
sessionId: this.sessionId,
followRedirects: options?.allowRedirects || false,
forceHttp1: this.sessionOptions.forceHttp1 || false,
headers,
headerOrder: this.sessionOptions.headerOrder,
insecureSkipVerify: options?.insecureSkipVerify || false,
isByteRequest: isBinaryRequest,
proxyUrl: proxy,
requestUrl: url,
requestMethod: method.toUpperCase(),
requestBody,
requestCookies: [],
timeoutSeconds: options?.timeoutSeconds || 30,
};
if (Object.keys(this.sessionOptions || {}).length) {
if (this.sessionOptions.clientIdentifier) {
requestPayload.tlsClientIdentifier = this.sessionOptions.clientIdentifier;
requestPayload.withRandomTLSExtensionOrder = this.sessionOptions.randomTlsExtensionOrder || false;
}
else {
requestPayload.customTlsClient = {
ja3String: this.sessionOptions.ja3string,
h2Settings: this.sessionOptions.h2Settings,
h2SettingsOrder: this.sessionOptions.h2SettingsOrder,
supportedSignatureAlgorithms: this.sessionOptions.supportedSignatureAlgorithms,
supportedVersions: this.sessionOptions.supportedVersions,
keyShareCurves: this.sessionOptions.keyShareCurves,
certCompressionAlgo: this.sessionOptions.certCompressionAlgo,
pseudoHeaderOrder: this.sessionOptions.pseudoHeaderOrder,
connectionFlow: this.sessionOptions.connectionFlow,
priorityFrames: this.sessionOptions.priorityFrames,
headerOrder: this.sessionOptions.headerOrder,
headerPriority: this.sessionOptions.headerPriority,
};
}
}
else {
requestPayload.tlsClientIdentifier = 'chrome_108';
requestPayload.withRandomTLSExtensionOrder = false;
}
const response = await new Promise((resolve, reject) => {
request.async(JSON.stringify(requestPayload), (error, response) => {
if (error)
reject(error);
else
resolve(response);
});
});
if (!response) {
throw new FingerPrintMeNotException('No response received');
}
// Parse JSON response with UTF-8 corruption protection
let responseObject;
try {
responseObject = JSON.parse(response);
// If we have binary data (indicated by body_len), validate the base64
if (responseObject.body_len && typeof responseObject.body_len === 'number') {
const base64Body = responseObject.body || '';
// Check for signs of UTF-8 corruption in base64 data
const validBase64Chars = /^[A-Za-z0-9+/]*={0,2}$/;
if (!validBase64Chars.test(base64Body.replace(/\s/g, ''))) {
console.warn('Warning: Base64 data appears to be corrupted by UTF-8 processing.');
console.warn('Original length:', base64Body.length);
console.warn('Invalid chars found in base64:', base64Body.replace(/[A-Za-z0-9+/=\s]/g, ''));
// Attempt to clean the base64 string
const cleanedBase64 = base64Body.replace(/[^A-Za-z0-9+/=]/g, '');
if (cleanedBase64 !== base64Body) {
console.warn('Attempting to clean corrupted base64 data...');
responseObject.body = cleanedBase64;
}
}
}
}
catch (parseError) {
console.error('JSON parsing failed. Response may be corrupted:', parseError);
console.error('Response preview:', response.substring(0, 200) + (response.length > 200 ? '...' : ''));
throw new FingerPrintMeNotException(`Failed to parse response from native library: ${parseError}`);
}
if (responseObject.status === 0) {
throw new FingerPrintMeNotException(responseObject.body);
}
const responseCookieJar = new CookieJar();
if (!(this.sessionOptions.ignoreResponseCookies ?? false)) {
if (responseObject.headers['Set-Cookie'] && Array.isArray(responseObject.headers['Set-Cookie'])) {
for (const cookie of responseObject.headers['Set-Cookie']) {
await responseCookieJar.setCookie(cookie, url);
}
}
}
return new Response(responseObject, responseCookieJar);
}
async get(url, options) {
return this.executeRequest('GET', url, options);
}
async post(url, options) {
return this.executeRequest('POST', url, options);
}
async put(url, options) {
return this.executeRequest('PUT', url, options);
}
async patch(url, options) {
return this.executeRequest('PATCH', url, options);
}
async delete(url, options) {
return this.executeRequest('DELETE', url, options);
}
async head(url, options) {
return this.executeRequest('HEAD', url, options);
}
async options(url, options) {
return this.executeRequest('OPTIONS', url, options);
}
}
//# sourceMappingURL=sessions.js.map